Merge pull request #40 from Discours/popup

common Popup component & SharePopup
This commit is contained in:
Igor Lobanov 2022-10-25 18:17:07 +02:00 committed by GitHub
commit 897afd6d06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 200 additions and 151 deletions

View File

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

6
.lintstagedrc.bak Normal file
View File

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

View File

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

View File

@ -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) => {
{/* </a>*/}
{/*</div>*/}
<div class="shout-stats__item">
<a href="#share" onClick={() => showModal('share')}>
<Icon name="share" />
</a>
<SharePopup
trigger={
<a href="#" onClick={(event) => event.preventDefault()}>
<Icon name="share" />
</a>
}
/>
</div>
{/*FIXME*/}
{/*<Show when={canEdit()}>*/}

View File

@ -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<PopupProps, 'children'>
export const SharePopup = (props: SharePopupProps) => {
return (
<Popup {...props}>
<ul class="nodash">
<li>
<a href="#">
<Icon name="vk-white" class={styles.icon} />
VK
</a>
</li>
<li>
<a href="#">
<Icon name="facebook-white" class={styles.icon} />
Facebook
</a>
</li>
<li>
<a href="#">
<Icon name="twitter-white" class={styles.icon} />
Twitter
</a>
</li>
<li>
<a href="#">
<Icon name="telegram-white" class={styles.icon} />
Telegram
</a>
</li>
<li>
<a href="#">
<Icon name="link-white" class={styles.icon} />
{t('Copy link')}
</a>
</li>
</ul>
</Popup>
)
}

View File

@ -1,7 +1,7 @@
import styles from './Banner.module.scss'
import { t } from '../../utils/intl'
import { showModal } from '../../stores/ui'
import {clsx} from "clsx";
import { clsx } from 'clsx'
export default () => {
return (

View File

@ -4,7 +4,7 @@ import { Icon } from '../Nav/Icon'
import Subscribe from './Subscribe'
import { t } from '../../utils/intl'
import { locale } from '../../stores/ui'
import {clsx} from "clsx";
import { clsx } from 'clsx'
export const Footer = () => {
const locale_title = createMemo(() => (locale() === 'ru' ? 'English' : 'Русский'))

View File

@ -7,7 +7,7 @@ export default (props: { article: Shout }) => (
<div class="floor floor--one-article">
<div class="wide-container row">
<div class="col-12">
<ArticleCard article={props.article} settings={{isSingle: true}} />
<ArticleCard article={props.article} settings={{ isSingle: true }} />
</div>
</div>
</div>

View File

@ -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;
@ -348,18 +336,15 @@
transform: translateY(-50%);
width: 100%;
.icon {
.control {
cursor: pointer;
margin-left: 1.6rem;
opacity: 0.6;
transition: opacity 0.3s;
}
border: 0;
img {
vertical-align: middle;
}
a {
border: none;
.icon {
opacity: 0.6;
transition: opacity 0.3s;
}
&:hover {
background: none;
@ -370,4 +355,8 @@
}
}
}
img {
vertical-align: middle;
}
}

View File

@ -3,18 +3,17 @@ 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, toggleModal} from '../../stores/ui'
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'
const log = getLogger('header')
@ -39,6 +38,7 @@ 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)
// stores
const { getWarnings } = useWarningsStore()
const { session } = useAuthStore()
@ -48,14 +48,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() || (getModal() && getModal() !== 'share');
document.body.classList.toggle('fixed', isFixed);
document.body.classList.toggle(styles.fixed, isFixed && !getModal());
}, [fixed(), getModal()])
document.body.classList.toggle('fixed', fixed() || getModal() !== null)
})
// derived
const authorized = createMemo(() => session()?.user?.slug)
@ -90,7 +87,7 @@ export const Header = (props: Props) => {
classList={{
[styles.headerFixed]: props.isHeaderFixed,
[styles.headerScrolledTop]: !getIsScrollingBottom() && getIsScrolled(),
[styles.headerScrolledBottom]: getIsScrollingBottom() && getIsScrolled(),
[styles.headerScrolledBottom]: (getIsScrollingBottom() && getIsScrolled()) || isSharePopupVisible(),
[styles.headerWithTitle]: Boolean(props.title)
}}
>
@ -99,41 +96,6 @@ export const Header = (props: Props) => {
</Modal>
<div class={clsx(styles.mainHeaderInner, 'wide-container')}>
<Popup name="share" class={clsx(styles.popupShare, stylesPopup.popupShare)}>
<ul class="nodash">
<li>
<a href="#">
<Icon name="vk-white" class={stylesPopup.icon}/>
VK
</a>
</li>
<li>
<a href="#">
<Icon name="facebook-white" class={stylesPopup.icon}/>
Facebook
</a>
</li>
<li>
<a href="#">
<Icon name="twitter-white" class={stylesPopup.icon}/>
Twitter
</a>
</li>
<li>
<a href="#">
<Icon name="telegram-white" class={stylesPopup.icon}/>
Telegram
</a>
</li>
<li>
<a href="#">
<Icon name="link-white" class={stylesPopup.icon}/>
{t('Copy link')}
</a>
</li>
</ul>
</Popup>
<nav class={clsx(styles.headerInner, 'row')} classList={{ fixed: fixed() }}>
<div class={clsx(styles.mainLogo, 'col-auto')}>
<a href={getPagePath(router, 'home')} onClick={handleClientRouteLinkClick}>
@ -197,14 +159,23 @@ export const Header = (props: Props) => {
</div>
<Show when={props.title}>
<div class={styles.articleControls}>
<button onClick={() => {toggleModal('share')}}>
<Icon name="share-outline" class={styles.icon}/>
</button>
<a href="#comments">
<SharePopup
onVisibilityChange={(isVisible) => {
console.log({ isVisible })
setIsSharePopupVisible(isVisible)
}}
containerCssClass={styles.control}
trigger={<Icon name="share-outline" class={styles.icon} />}
/>
<a href="#comments" class={styles.control}>
<Icon name="comments-outline" class={styles.icon} />
</a>
<Icon name="pencil-outline" class={styles.icon} />
<Icon name="bookmark" class={styles.icon} />
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>
<Icon name="pencil-outline" class={styles.icon} />
</a>
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>
<Icon name="bookmark" class={styles.icon} />
</a>
</div>
</Show>
</div>

View File

@ -1,6 +1,13 @@
.container {
position: relative;
}
.popup {
background: #fff;
border: 2px solid #000;
top: calc(100% + 8px);
transform: translateX(-50%);
opacity: 1;
@include font-size(1.6rem);
@ -24,6 +31,7 @@
a {
border: none;
white-space: nowrap;
}
img {
@ -40,7 +48,14 @@
}
}
.popupShare {
right: 1em;
top: 4.5rem;
}
// TODO: animation
// .popup {
// opacity: 1;
// transition: opacity 0.3s;
// z-index: 1;
// &.visible {
// opacity: 0;
// transition: opacity 0.3s, z-index 0s 0.3s;
// z-index: -1;
// }
// }

View File

@ -1,33 +1,50 @@
import { createEffect, createSignal, onMount, Show } from 'solid-js'
import style from './Popup.module.scss'
import { hideModal, useModalStore } from '../../stores/ui'
import {clsx} from 'clsx';
import { createEffect, createSignal, JSX, onCleanup, onMount, Show } from 'solid-js'
import styles from './Popup.module.scss'
import { clsx } from 'clsx'
interface PopupProps {
name: string
children: any
class?: string
export type PopupProps = {
containerCssClass?: string
trigger: JSX.Element
children: JSX.Element
onVisibilityChange?: (isVisible) => void
}
export const Popup = (props: PopupProps) => {
const { getModal } = useModalStore()
const [isVisible, setIsVisible] = createSignal(false)
createEffect(() => {
if (props.onVisibilityChange) {
props.onVisibilityChange(isVisible())
}
})
let container: HTMLDivElement | undefined
const handleClickOutside = (event: MouseEvent & { target: Element }) => {
if (!isVisible()) {
return
}
if (event.target === container || container?.contains(event.target)) {
return
}
setIsVisible(false)
}
onMount(() => {
window.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') hideModal()
})
})
const [visible, setVisible] = createSignal(false)
createEffect(() => {
setVisible(getModal() === props.name)
document.addEventListener('click', handleClickOutside, { capture: true })
onCleanup(() => document.removeEventListener('click', handleClickOutside, { capture: true }))
})
const toggle = () => setIsVisible((oldVisible) => !oldVisible)
// class={clsx(styles.popupShare, stylesPopup.popupShare)}
return (
<Show when={visible()}>
<div class={clsx(style.popup, props.class)}>
{props.children}
</div>
</Show>
<span class={clsx(styles.container, props.containerCssClass)} ref={container}>
<span onClick={toggle}>{props.trigger}</span>
<Show when={isVisible()}>
<div class={clsx(styles.popup)}>{props.children}</div>
</Show>
</span>
)
}

View File

@ -54,28 +54,29 @@ export const ManifestPage = () => {
<div class="col-lg-10 offset-md-1">
<p>
Дискурс&nbsp;&mdash; независимый художественно-аналитический журнал с&nbsp;горизонтальной редакцией,
основанный на&nbsp;принципах свободы слова, прямой демократии и&nbsp;совместного редактирования.
Дискурс создаётся открытым медиасообществом ученых, журналистов, музыкантов, писателей,
предпринимателей, философов, инженеров, художников и&nbsp;специалистов со&nbsp;всего мира,
объединившихся, чтобы вместе делать общий журнал и&nbsp;объяснять с&nbsp;разных точек
зрения мозаичную картину современности.
Дискурс&nbsp;&mdash; независимый художественно-аналитический журнал с&nbsp;горизонтальной
редакцией, основанный на&nbsp;принципах свободы слова, прямой демократии и&nbsp;совместного
редактирования. Дискурс создаётся открытым медиасообществом ученых, журналистов, музыкантов,
писателей, предпринимателей, философов, инженеров, художников и&nbsp;специалистов
со&nbsp;всего мира, объединившихся, чтобы вместе делать общий журнал и&nbsp;объяснять
с&nbsp;разных точек зрения мозаичную картину современности.
</p>
<p>
Мы&nbsp;пишем о&nbsp;культуре, науке и&nbsp;обществе, рассказываем о&nbsp;новых идеях и&nbsp;современном искусстве,
публикуем статьи, исследования, репортажи, интервью людей, чью прямую речь стоит услышать,
и&nbsp;работы художников из&nbsp;разных стран&nbsp;&mdash; от&nbsp;фильмов и&nbsp;музыки
до&nbsp;живописи и&nbsp;фотографии. Помогая друг другу делать публикации качественнее
и&nbsp;общим голосованием выбирая лучшие материалы для журнала, мы&nbsp;создаём новую
горизонтальную журналистику, чтобы честно рассказывать о&nbsp;важном и&nbsp;интересном.
Мы&nbsp;пишем о&nbsp;культуре, науке и&nbsp;обществе, рассказываем о&nbsp;новых идеях
и&nbsp;современном искусстве, публикуем статьи, исследования, репортажи, интервью людей, чью
прямую речь стоит услышать, и&nbsp;работы художников из&nbsp;разных стран&nbsp;&mdash;
от&nbsp;фильмов и&nbsp;музыки до&nbsp;живописи и&nbsp;фотографии. Помогая друг другу делать
публикации качественнее и&nbsp;общим голосованием выбирая лучшие материалы для журнала,
мы&nbsp;создаём новую горизонтальную журналистику, чтобы честно рассказывать о&nbsp;важном
и&nbsp;интересном.
</p>
<p>
Редакция Дискурса открыта для всех: у&nbsp;нас нет цензуры, запретных тем и&nbsp;идеологических рамок.
Каждый может <a href="/create">прислать материал</a> в&nbsp;журнал
и&nbsp;<a href="/about/guide">присоединиться к&nbsp;редакции</a>. Предоставляя трибуну
для независимой журналистики и&nbsp;художественных проектов, мы&nbsp;помогаем людям
рассказывать свои истории так, чтобы они были услышаны. Мы&nbsp;убеждены: чем больше
голосов будет звучать на&nbsp;Дискурсе, тем громче в&nbsp;полифонии мнений будет слышна истина.
Редакция Дискурса открыта для всех: у&nbsp;нас нет цензуры, запретных тем
и&nbsp;идеологических рамок. Каждый может <a href="/create">прислать материал</a>{' '}
в&nbsp;журнал и&nbsp;<a href="/about/guide">присоединиться к&nbsp;редакции</a>. Предоставляя
трибуну для независимой журналистики и&nbsp;художественных проектов, мы&nbsp;помогаем людям
рассказывать свои истории так, чтобы они были услышаны. Мы&nbsp;убеждены: чем больше голосов
будет звучать на&nbsp;Дискурсе, тем громче в&nbsp;полифонии мнений будет слышна истина.
</p>
</div>
@ -91,23 +92,26 @@ export const ManifestPage = () => {
</p>
<h3 id="contribute">Предлагать материалы</h3>
<p>
<a href="/create">Создавайте</a> свои статьи и&nbsp;художественные работы&nbsp;&mdash; лучшие из них будут
опубликованы в&nbsp;журнале. Дискурс&nbsp;&mdash; некоммерческое издание, авторы публикуются
в&nbsp;журнале на&nbsp;общественных началах, получая при этом <a href="/create?collab=true">поддержку</a> редакции,
право голоса, множество других возможностей и&nbsp;читателей по&nbsp;всему миру.
<a href="/create">Создавайте</a> свои статьи и&nbsp;художественные работы&nbsp;&mdash;
лучшие из них будут опубликованы в&nbsp;журнале. Дискурс&nbsp;&mdash; некоммерческое
издание, авторы публикуются в&nbsp;журнале на&nbsp;общественных началах, получая при этом{' '}
<a href="/create?collab=true">поддержку</a> редакции, право голоса, множество других
возможностей и&nbsp;читателей по&nbsp;всему миру.
</p>
<h3 id="donate">Поддерживать проект</h3>
<p>Дискурс существует на&nbsp;пожертвования читателей. Если вам нравится журнал, пожалуйста,</p>
<p>
<a href="/about/help">поддержите</a> нашу работу. Ваши пожертвования пойдут на&nbsp;выпуск новых
материалов, оплату серверов, труда программистов, дизайнеров и&nbsp;редакторов.
Дискурс существует на&nbsp;пожертвования читателей. Если вам нравится журнал, пожалуйста,
</p>
<p>
<a href="/about/help">поддержите</a> нашу работу. Ваши пожертвования пойдут на&nbsp;выпуск
новых материалов, оплату серверов, труда программистов, дизайнеров и&nbsp;редакторов.
</p>
<h3 id="cooperation">Сотрудничать с&nbsp;журналом</h3>
<p>
Мы всегда открыты для сотрудничества и&nbsp;рады единомышленникам. Если вы хотите помогать
журналу с&nbsp;редактурой, корректурой, иллюстрациями, переводами, версткой, подкастами,
мероприятиями, фандрайзингом или как-то ещё&nbsp;&mdash; скорее пишите нам
на&nbsp;<a href="mailto:welcome@discours.io">welcome@discours.io</a>.
мероприятиями, фандрайзингом или как-то ещё&nbsp;&mdash; скорее пишите нам на&nbsp;
<a href="mailto:welcome@discours.io">welcome@discours.io</a>.
</p>
<p>
Если вы представляете некоммерческую организацию и&nbsp;хотите сделать с&nbsp;нами
@ -116,25 +120,26 @@ export const ManifestPage = () => {
</p>
<p>
Если вы разработчик и&nbsp;хотите помогать с&nbsp;развитием сайта Дискурса,{' '}
<a href="mailto:services@discours.io">присоединяйтесь к&nbsp;IT-команде самиздата</a>. Открытый
код платформы для независимой журналистики, а&nbsp;также всех наших спецпроектов
и&nbsp;медиаинструментов находится <a href="https://github.com/Discours">в&nbsp;свободном доступе на&nbsp;GitHub</a>.
<a href="mailto:services@discours.io">присоединяйтесь к&nbsp;IT-команде самиздата</a>.
Открытый код платформы для независимой журналистики, а&nbsp;также всех наших спецпроектов
и&nbsp;медиаинструментов находится{' '}
<a href="https://github.com/Discours">в&nbsp;свободном доступе на&nbsp;GitHub</a>.
</p>
<h3 id="follow">Как еще можно помочь</h3>
<p>
Советуйте Дискурс друзьям и&nbsp;знакомым. Обсуждайте и&nbsp;распространяйте наши
публикации&nbsp;&mdash; все материалы открытой редакции можно читать и&nbsp;перепечатывать
бесплатно. Подпишитесь на&nbsp;самиздат{' '}
<a href="https://vk.com/discoursio">ВКонтакте</a>,
бесплатно. Подпишитесь на&nbsp;самиздат <a href="https://vk.com/discoursio">ВКонтакте</a>,
в&nbsp;<a href="https://facebook.com/discoursio">Фейсбуке</a>
и&nbsp;в&nbsp;<a href="https://t.me/discoursio">Телеграме</a>, а&nbsp;также
на&nbsp;<Opener name="subscribe">рассылку лучших материалов</Opener>,
чтобы не&nbsp;пропустить ничего интересного.
и&nbsp;в&nbsp;<a href="https://t.me/discoursio">Телеграме</a>, а&nbsp;также на&nbsp;
<Opener name="subscribe">рассылку лучших материалов</Opener>, чтобы не&nbsp;пропустить
ничего интересного.
</p>
<p>
<a href="https://forms.gle/9UnHBAz9Q3tjH5dAA">Рассказывайте о&nbsp;впечатлениях</a>
от&nbsp;материалов открытой редакции, <Opener name="feedback">делитесь идеями</Opener>,
интересными темами, о&nbsp;которых хотели бы узнать больше, и&nbsp;историями, которые нужно рассказать.
интересными темами, о&nbsp;которых хотели бы узнать больше, и&nbsp;историями, которые нужно
рассказать.
</p>
</div>
@ -145,9 +150,9 @@ export const ManifestPage = () => {
<div class="col-lg-10 offset-md-1">
Если вы хотите предложить материал, сотрудничать, рассказать о&nbsp;проблеме, которую нужно
осветить, сообщить об&nbsp;ошибке или баге, что-то обсудить, уточнить или посоветовать,
пожалуйста, <Opener name="feedback">напишите нам здесь</Opener> или
на&nbsp;почту <a href="mailto:welcome@discours.io">welcome@discours.io</a>. Мы обязательно
ответим и&nbsp;постараемся реализовать все хорошие задумки.
пожалуйста, <Opener name="feedback">напишите нам здесь</Opener> или на&nbsp;почту{' '}
<a href="mailto:welcome@discours.io">welcome@discours.io</a>. Мы обязательно ответим
и&nbsp;постараемся реализовать все хорошие задумки.
</div>
</div>
</div>

View File

@ -5,7 +5,7 @@ import { createSignal } from 'solid-js'
//export const locale = persistentAtom<string>('locale', 'ru')
export const [locale, setLocale] = createSignal('ru')
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'share' | 'thank' | 'donate' | null
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'thank' | 'donate' | null
type WarnKind = 'error' | 'warn' | 'info'
export interface Warning {
@ -20,7 +20,6 @@ const warnings = atom<Warning[]>([])
export const showModal = (modalType: ModalType) => modal.set(modalType)
export const hideModal = () => modal.set(null)
export const toggleModal = (modalType) => modal.get() ? hideModal() : showModal(modalType)
export const clearWarns = () => warnings.set([])
export const warn = (warning: Warning) => warnings.set([...warnings.get(), warning])