Markup for mobile fix-pack (#349)

* mediaQuery context provider
* Fix header styles
* User list markup fix
This commit is contained in:
Ilya Y 2023-12-29 09:39:16 +03:00 committed by GitHub
parent 41b5560036
commit 11d3a6c274
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 198 additions and 110 deletions

View File

@ -7,6 +7,7 @@ import { Dynamic } from 'solid-js/web'
import { ConfirmProvider } from '../context/confirm' import { ConfirmProvider } from '../context/confirm'
import { EditorProvider } from '../context/editor' import { EditorProvider } from '../context/editor'
import { LocalizeProvider } from '../context/localize' import { LocalizeProvider } from '../context/localize'
import { MediaQueryProvider } from '../context/mediaQuery'
import { NotificationsProvider } from '../context/notifications' import { NotificationsProvider } from '../context/notifications'
import { SessionProvider } from '../context/session' import { SessionProvider } from '../context/session'
import { SnackbarProvider } from '../context/snackbar' import { SnackbarProvider } from '../context/snackbar'
@ -115,6 +116,7 @@ export const App = (props: Props) => {
<MetaProvider> <MetaProvider>
<Meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta name="viewport" content="width=device-width, initial-scale=1" />
<LocalizeProvider> <LocalizeProvider>
<MediaQueryProvider>
<SnackbarProvider> <SnackbarProvider>
<ConfirmProvider> <ConfirmProvider>
<SessionProvider> <SessionProvider>
@ -126,6 +128,7 @@ export const App = (props: Props) => {
</SessionProvider> </SessionProvider>
</ConfirmProvider> </ConfirmProvider>
</SnackbarProvider> </SnackbarProvider>
</MediaQueryProvider>
</LocalizeProvider> </LocalizeProvider>
</MetaProvider> </MetaProvider>
) )

View File

@ -2,7 +2,6 @@
align-items: flex-start; align-items: flex-start;
display: flex; display: flex;
gap: 1rem; gap: 1rem;
margin-bottom: 3rem;
&.nameOnly { &.nameOnly {
align-items: center; align-items: center;

View File

@ -1,8 +1,9 @@
import { openPage } from '@nanostores/router' import { openPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js' import { createEffect, createMemo, createSignal, Match, Show, Switch } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useMediaQuery } from '../../../context/mediaQuery'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { Author, FollowingEntity } from '../../../graphql/types.gen' import { Author, FollowingEntity } from '../../../graphql/types.gen'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
@ -23,7 +24,14 @@ type Props = {
nameOnly?: boolean nameOnly?: boolean
} }
export const AuthorBadge = (props: Props) => { export const AuthorBadge = (props: Props) => {
const { mediaMatches } = useMediaQuery()
const [isMobileView, setIsMobileView] = createSignal(false)
const [isSubscribing, setIsSubscribing] = createSignal(false) const [isSubscribing, setIsSubscribing] = createSignal(false)
createEffect(() => {
setIsMobileView(!mediaMatches.sm)
})
const { const {
session, session,
subscriptions, subscriptions,
@ -65,7 +73,7 @@ export const AuthorBadge = (props: Props) => {
<div class={styles.basicInfo}> <div class={styles.basicInfo}>
<Userpic <Userpic
hasLink={true} hasLink={true}
size={'M'} size={isMobileView() ? 'M' : 'L'}
name={props.author.name} name={props.author.name}
userpic={props.author.userpic} userpic={props.author.userpic}
slug={props.author.slug} slug={props.author.slug}
@ -111,7 +119,7 @@ export const AuthorBadge = (props: Props) => {
fallback={ fallback={
<Button <Button
variant={props.iconButtons ? 'secondary' : 'bordered'} variant={props.iconButtons ? 'secondary' : 'bordered'}
size="M" size="S"
value={ value={
<Show <Show
when={props.iconButtons} when={props.iconButtons}
@ -135,7 +143,7 @@ export const AuthorBadge = (props: Props) => {
> >
<Button <Button
variant={props.iconButtons ? 'secondary' : 'bordered'} variant={props.iconButtons ? 'secondary' : 'bordered'}
size="M" size="S"
value={ value={
<Show <Show
when={props.iconButtons} when={props.iconButtons}
@ -161,7 +169,7 @@ export const AuthorBadge = (props: Props) => {
<Show when={props.showMessageButton}> <Show when={props.showMessageButton}>
<Button <Button
variant={props.iconButtons ? 'secondary' : 'bordered'} variant={props.iconButtons ? 'secondary' : 'bordered'}
size="M" size="S"
value={props.iconButtons ? <Icon name="inbox-white" /> : t('Message')} value={props.iconButtons ? <Icon name="inbox-white" /> : t('Message')}
onClick={initChat} onClick={initChat}
class={clsx(styles.actionButton, { [styles.iconed]: props.iconButtons })} class={clsx(styles.actionButton, { [styles.iconed]: props.iconButtons })}

View File

@ -28,7 +28,6 @@ type Props = {
followers?: Author[] followers?: Author[]
following?: Array<Author | Topic> following?: Array<Author | Topic>
} }
export const AuthorCard = (props: Props) => { export const AuthorCard = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { const {
@ -258,7 +257,7 @@ export const AuthorCard = (props: Props) => {
</Show> </Show>
</ShowOnlyOnClient> </ShowOnlyOnClient>
<Show when={props.followers}> <Show when={props.followers}>
<Modal variant="medium" name="followers" maxHeight> <Modal variant="medium" isResponsive={true} name="followers" maxHeight>
<> <>
<h2>{t('Followers')}</h2> <h2>{t('Followers')}</h2>
<div class={styles.listWrapper}> <div class={styles.listWrapper}>
@ -274,7 +273,7 @@ export const AuthorCard = (props: Props) => {
</Modal> </Modal>
</Show> </Show>
<Show when={props.following}> <Show when={props.following}>
<Modal variant="medium" name="following" maxHeight> <Modal variant="medium" isResponsive={true} name="following" maxHeight>
<> <>
<h2>{t('Subscriptions')}</h2> <h2>{t('Subscriptions')}</h2>
<ul class="view-switcher"> <ul class="view-switcher">

View File

@ -5,7 +5,7 @@
margin-bottom: 2.2rem; margin-bottom: 2.2rem;
position: absolute; position: absolute;
width: 100%; width: 100%;
z-index: 10000; z-index: 10003;
.wide-container { .wide-container {
background: #fff; background: #fff;
@ -149,7 +149,7 @@
position: fixed; position: fixed;
top: 58px; top: 58px;
width: 100%; width: 100%;
z-index: 1; z-index: 10003;
li { li {
margin-bottom: 2.4rem !important; margin-bottom: 2.4rem !important;

View File

@ -61,7 +61,10 @@ export const Header = (props: Props) => {
const [isTopicsVisible, setIsTopicsVisible] = createSignal(false) const [isTopicsVisible, setIsTopicsVisible] = createSignal(false)
const [isZineVisible, setIsZineVisible] = createSignal(false) const [isZineVisible, setIsZineVisible] = createSignal(false)
const [isFeedVisible, setIsFeedVisible] = createSignal(false) const [isFeedVisible, setIsFeedVisible] = createSignal(false)
const toggleFixed = () => setFixed((oldFixed) => !oldFixed) const toggleFixed = () => {
setFixed(!fixed())
console.log('!!! toggleFixed:')
}
const tag = (topic: Topic) => const tag = (topic: Topic) =>
/[ЁА-яё]/.test(topic.title || '') && lang() !== 'ru' ? topic.slug : topic.title /[ЁА-яё]/.test(topic.title || '') && lang() !== 'ru' ? topic.slug : topic.title
@ -138,6 +141,9 @@ export const Header = (props: Props) => {
clearTimeout(timer) clearTimeout(timer)
} }
createEffect(() => {
console.log('!!! mo:', modal())
})
const hideSubnavigation = (event, time = 500) => { const hideSubnavigation = (event, time = 500) => {
timer = setTimeout(() => { timer = setTimeout(() => {
toggleSubnavigation(false) toggleSubnavigation(false)
@ -183,9 +189,9 @@ export const Header = (props: Props) => {
</Modal> </Modal>
<div class={clsx(styles.mainHeaderInner, 'wide-container')}> <div class={clsx(styles.mainHeaderInner, 'wide-container')}>
<nav class={clsx('row', styles.headerInner, { ['fixed']: fixed() })}> <nav class={clsx('row', styles.headerInner, { [styles.fixed]: fixed() })}>
<div class={clsx(styles.burgerContainer, 'col-auto')}> <div class={clsx(styles.burgerContainer, 'col-auto')}>
<div class={styles.burger} classList={{ fixed: fixed() }} onClick={toggleFixed}> <div class={clsx(styles.burger, { [styles.fixed]: fixed() })} onClick={toggleFixed}>
<div /> <div />
</div> </div>
</div> </div>

View File

@ -10,11 +10,11 @@
position: fixed; position: fixed;
top: 0; top: 0;
width: 100%; width: 100%;
z-index: 10002; z-index: 10003;
} }
.modal { .modal {
background: #fff; background: var(--background-color);
max-width: 1000px; max-width: 1000px;
position: relative; position: relative;
@ -114,3 +114,23 @@
flex-direction: column; flex-direction: column;
height: 90vh; height: 90vh;
} }
.backdrop.isMobile {
z-index: 10002;
top: 56px;
height: calc(100% - 58px);
bottom: 0;
.maxHeight {
height: 100%;
}
.container {
padding: 0;
height: 100%;
min-height: 100%;
}
.modalInner {
padding: 1rem 1rem 0;
height: 100%;
}
}

View File

@ -4,6 +4,7 @@ import { redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, Show } from 'solid-js' import { createEffect, createMemo, createSignal, Show } from 'solid-js'
import { useMediaQuery } from '../../../context/mediaQuery'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
import { hideModal, useModalStore } from '../../../stores/ui' import { hideModal, useModalStore } from '../../../stores/ui'
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler' import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
@ -19,12 +20,15 @@ interface Props {
noPadding?: boolean noPadding?: boolean
maxHeight?: boolean maxHeight?: boolean
allowClose?: boolean allowClose?: boolean
isResponsive?: boolean
} }
export const Modal = (props: Props) => { export const Modal = (props: Props) => {
const { modal } = useModalStore() const { modal } = useModalStore()
const [visible, setVisible] = createSignal(false) const [visible, setVisible] = createSignal(false)
const allowClose = createMemo(() => props.allowClose !== false) const allowClose = createMemo(() => props.allowClose !== false)
const [isMobileView, setIsMobileView] = createSignal(false)
const { mediaMatches } = useMediaQuery()
const handleHide = () => { const handleHide = () => {
if (modal()) { if (modal()) {
if (allowClose()) { if (allowClose()) {
@ -33,7 +37,6 @@ export const Modal = (props: Props) => {
redirectPage(router, 'home') redirectPage(router, 'home')
} }
} }
hideModal() hideModal()
} }
@ -43,10 +46,21 @@ export const Modal = (props: Props) => {
setVisible(modal() === props.name) setVisible(modal() === props.name)
}) })
createEffect(() => {
if (props.isResponsive) {
setIsMobileView(!mediaMatches.sm)
}
})
return ( return (
<Show when={visible()}> <Show when={visible()}>
<div class={styles.backdrop} onClick={handleHide}> <div
<div class="wide-container"> class={clsx(styles.backdrop, {
[styles.isMobile]: isMobileView(),
})}
onClick={handleHide}
>
<div class={clsx('wide-container', styles.container)}>
<div <div
class={clsx(styles.modal, { class={clsx(styles.modal, {
[styles.narrow]: props.variant === 'narrow', [styles.narrow]: props.variant === 'narrow',
@ -57,9 +71,11 @@ export const Modal = (props: Props) => {
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
> >
<div class={styles.modalInner}>{props.children}</div> <div class={styles.modalInner}>{props.children}</div>
<Show when={!isMobileView()}>
<div class={styles.close} onClick={handleHide}> <div class={styles.close} onClick={handleHide}>
<Icon name="close" class={styles.icon} /> <Icon name="close" class={styles.icon} />
</div> </div>
</Show>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,8 +6,10 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
transform: translateY(-2px); transform: translateY(-2px);
width: 100%;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
overflow: auto;
padding: 0 divide($container-padding-x, 2); padding: 0 divide($container-padding-x, 2);
} }

View File

@ -3,14 +3,15 @@
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
margin-bottom: 2rem; margin-bottom: 2rem;
gap: 1rem;
@include media-breakpoint-down(sm) { .basicInfo {
flex-wrap: wrap; display: flex;
margin-bottom: 3rem; flex-direction: row;
} align-items: flex-start;
flex-wrap: nowrap;
@include media-breakpoint-down(md) { flex: 1;
text-align: left; gap: 1rem;
} }
.picture { .picture {
@ -24,7 +25,12 @@
background-position: 50% 50%; background-position: 50% 50%;
background-repeat: no-repeat; background-repeat: no-repeat;
border: none; border: none;
margin-right: 1.2rem;
&.smallSize {
width: 32px;
height: 32px;
min-width: 32px;
}
&:hover { &:hover {
background-color: var(--black-50); background-color: var(--black-50);
@ -40,21 +46,18 @@
border: none; border: none;
display: flex; display: flex;
flex: 0 calc(100% - 5.2rem);
flex-direction: column; flex-direction: column;
margin-bottom: 1rem;
@include media-breakpoint-up(sm) {
flex: 1 100%;
}
&:hover { &:hover {
background: unset; background: unset;
} }
.title { .title {
@include font-size(1.4rem);
font-weight: 500;
line-height: 1em;
color: var(--blue-500); color: var(--blue-500);
font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
} }
@ -64,19 +67,9 @@
} }
.actions { .actions {
flex: 0 20%; display: flex;
margin-left: 5.2rem; flex-direction: row;
gap: 1rem;
@include media-breakpoint-up(sm) {
margin-left: 2rem;
}
@include media-breakpoint-up(md) {
flex: 1;
margin-left: auto;
padding-left: 1rem;
text-align: right;
}
} }
.subscribeButton { .subscribeButton {

View File

@ -1,7 +1,8 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, createSignal, Show } from 'solid-js' import { createEffect, createMemo, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useMediaQuery } from '../../../context/mediaQuery'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/types.gen' import { FollowingEntity, Topic } from '../../../graphql/types.gen'
import { follow, unfollow } from '../../../stores/zine/common' import { follow, unfollow } from '../../../stores/zine/common'
@ -17,10 +18,14 @@ type Props = {
} }
export const TopicBadge = (props: Props) => { export const TopicBadge = (props: Props) => {
const [isSubscribing, setIsSubscribing] = createSignal(false)
const { t } = useLocalize() const { t } = useLocalize()
const { mediaMatches } = useMediaQuery()
const [isMobileView, setIsMobileView] = createSignal(false)
const [isSubscribing, setIsSubscribing] = createSignal(false)
createEffect(() => {
setIsMobileView(!mediaMatches.sm)
})
const { const {
isAuthenticated,
subscriptions, subscriptions,
actions: { loadSubscriptions }, actions: { loadSubscriptions },
} = useSession() } = useSession()
@ -42,9 +47,13 @@ export const TopicBadge = (props: Props) => {
return ( return (
<div class={styles.TopicBadge}> <div class={styles.TopicBadge}>
<div class={styles.basicInfo}>
<a <a
href={`/topic/${props.topic.slug}`} href={`/topic/${props.topic.slug}`}
class={clsx(styles.picture, { [styles.withImage]: props.topic.pic })} class={clsx(styles.picture, {
[styles.withImage]: props.topic.pic,
[styles.smallSize]: isMobileView(),
})}
style={ style={
props.topic.pic && { props.topic.pic && {
'background-image': `url('${getImageUrl(props.topic.pic, { width: 40, height: 40 })}')`, 'background-image': `url('${getImageUrl(props.topic.pic, { width: 40, height: 40 })}')`,
@ -64,16 +73,13 @@ export const TopicBadge = (props: Props) => {
<div class={clsx('text-truncate', styles.description)}>{props.topic.body}</div> <div class={clsx('text-truncate', styles.description)}>{props.topic.body}</div>
</Show> </Show>
</a> </a>
<Show when={isAuthenticated()}> </div>
<div class={styles.actions}> <div class={styles.actions}>
<Show <Show
when={!props.minimizeSubscribeButton} when={!props.minimizeSubscribeButton}
fallback={ fallback={
<CheckButton <CheckButton text={t('Follow')} checked={subscribed()} onClick={() => subscribe(!subscribed)} />
text={t('Follow')}
checked={subscribed()}
onClick={() => subscribe(!subscribed)}
/>
} }
> >
<Show <Show
@ -98,7 +104,6 @@ export const TopicBadge = (props: Props) => {
</Show> </Show>
</Show> </Show>
</div> </div>
</Show>
</div> </div>
) )
} }

View File

@ -75,4 +75,9 @@
.viewSwitcher { .viewSwitcher {
margin-bottom: 2rem; margin-bottom: 2rem;
width: 100%;
@include media-breakpoint-down(sm) {
overflow-x: auto;
}
} }

View File

@ -2,6 +2,7 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
position: relative; position: relative;
min-width: 100px;
&.bordered { &.bordered {
border: 2px solid var(--black-100); border: 2px solid var(--black-100);

View File

@ -0,0 +1,31 @@
import type { JSX } from 'solid-js'
import { createBreakpoints } from '@solid-primitives/media'
import { createContext, useContext } from 'solid-js'
const breakpoints = {
xs: '0',
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px',
xxl: '1400px',
}
type MediaQueryContextType = {
mediaMatches: ReturnType<typeof createBreakpoints>
}
const MediaQueryContext = createContext<MediaQueryContextType>()
export function useMediaQuery() {
return useContext(MediaQueryContext)
}
export const MediaQueryProvider = (props: { children: JSX.Element }) => {
const mediaMatches = createBreakpoints(breakpoints)
const value: MediaQueryContextType = { mediaMatches }
return <MediaQueryContext.Provider value={value}>{props.children}</MediaQueryContext.Provider>
}