Merge pull request #24 from tmshv/fix/menu-refactoring

New menu
This commit is contained in:
tmshv authored and GitHub committed 2020-02-23 07:59:56 +03:00
commit 5375165d9b
42 files changed
+501 -643

No files matched your search

+17 -6
View File
@@ -2,11 +2,11 @@ import Head from 'next/head'
import { App } from 'src/components/App'
import { tail } from 'lodash'
import { Page } from 'src/components/Page'
import menuModel from 'src/models/menu'
import { buildMenu } from 'src/lib/menu'
import { Meta } from 'src/components/Meta'
import { meta } from 'src/lib/meta'
import { createApiUrl, requestGet, wrapInitialProps, IResponseItems } from 'src/next-lib'
import { NextPage } from 'next'
import { IBreadcumbsPart, IMeta, IPage } from 'src/types'
function array<T>(value: T | T[]) {
return Array.isArray(value)
@@ -14,13 +14,21 @@ function array<T>(value: T | T[]) {
: [value]
}
const Index = props => (
type Props = {
page: any
title: string
content: string
breadcrumb: IBreadcumbsPart[]
meta: IMeta
}
const Index: NextPage<Props> = props => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
contentStyle={{
marginTop: 'var(--size-l)'
}}
breadcrumbs={props.breadcrumb}
>
<Head>
<title>{props.title}</title>
@@ -42,16 +50,18 @@ export const unstable_getStaticProps = async (ctx: any) => {
} else {
slug = '/' + array(ctx.params.slug).join('/')
}
const page = await requestGet(createApiUrl(`/api/page?page=${slug}`), null)
const page = await requestGet<IPage>(createApiUrl(`/api/page?page=${slug}`), null)
if (!page) {
throw new Error(`Not found: ${slug}`)
}
const image = page.preview ? page.preview.artifacts.fb : {}
const breadcrumbSize = page.breadcrumb?.length ?? 0
const breadcrumb = breadcrumbSize < 2 ? null : page.breadcrumb
return {
props: {
page,
content: page.data,
pageUrl: slug,
title: page.title,
meta: meta({
title: page.title,
@@ -59,6 +69,7 @@ export const unstable_getStaticProps = async (ctx: any) => {
imageWidth: image.width,
imageHeight: image.height,
}),
breadcrumb,
}
}
}
-6
View File
@@ -2,13 +2,10 @@ import Head from 'next/head'
import { App } from 'src/components/App'
import { Article } from 'src/components/Article'
import { Html } from 'src/components/Html'
import menuModel from 'src/models/menu'
import { buildMenu } from 'src/lib/menu'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
import { NextPage } from 'next'
type Props = {
pageUrl: string
data: any //(album)
}
@@ -21,7 +18,6 @@ const Page: NextPage<Props> = props => {
return (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
>
<Head>
@@ -43,7 +39,6 @@ const Page: NextPage<Props> = props => {
}
export const unstable_getStaticProps = async (ctx: any) => {
const pageUrl = '/gallery'
// const id = ctx.query.slug
const id = ctx.params.slug
const data = await requestGet(createApiUrl(`/api/albums/${id}`), null)
@@ -55,7 +50,6 @@ export const unstable_getStaticProps = async (ctx: any) => {
return {
props: {
data,
pageUrl,
}
}
}
-5
View File
@@ -3,8 +3,6 @@ import { get } from 'lodash'
import { App } from 'src/components/App'
import { Article } from 'src/components/Article'
import { Html } from 'src/components/Html'
import menuModel from 'src/models/menu'
import { buildMenu } from 'src/lib/menu'
import { Meta } from 'src/components/Meta'
import { meta } from 'src/lib/meta'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
@@ -12,7 +10,6 @@ import { NextPage } from 'next'
import { IArticle } from 'src/types'
type Props = {
pageUrl: string
meta: any
data: IArticle
}
@@ -25,7 +22,6 @@ const Page: NextPage<Props> = props => {
return (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
>
<Head>
@@ -59,7 +55,6 @@ export const unstable_getStaticProps = async (ctx: any) => {
return {
props: {
data,
pageUrl,
meta: meta({
title: data.title,
image: image.src,
-6
View File
@@ -2,8 +2,6 @@ import Head from 'next/head'
import { range } from 'lodash'
import { App } from 'src/components/App'
import { ArticleGrid } from 'src/components/ArticleGrid'
import menuModel from 'src/models/menu'
import { buildMenu } from 'src/lib/menu'
import { meta } from 'src/lib/meta'
import { Meta } from 'src/components/Meta'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
@@ -12,7 +10,6 @@ import { NextPage } from 'next'
const pageSize = Number(process.env.APP_ARTICLES_PAGE_SIZE)
type Props = {
pageUrl: string
meta: any
title: string
data: any //(list of article)
@@ -28,7 +25,6 @@ const Page: NextPage<Props> = props => {
return (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
wide={true}
>
@@ -46,7 +42,6 @@ const Page: NextPage<Props> = props => {
}
export const unstable_getStaticProps = async (ctx: any) => {
const pageUrl = '/'
// const page = parseInt(ctx.query.page)
const page = parseInt(ctx.params.slug)
const res = await requestGet<IResponseItems<any[]>>(
@@ -67,7 +62,6 @@ export const unstable_getStaticProps = async (ctx: any) => {
data,
nextPage,
prevPage,
pageUrl,
title,
meta: meta({
title,
-7
View File
@@ -3,8 +3,6 @@ import { get } from 'lodash'
import { App } from 'src/components/App'
import { Meta } from 'src/components/Meta'
import { CollectiveImage } from 'src/components/CollectiveImage'
import menuModel from 'src/models/menu'
import { buildMenu } from 'src/lib/menu'
import { meta } from 'src/lib/meta'
import { IMeta, IImage } from 'src/types'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
@@ -20,7 +18,6 @@ interface IPerson {
}
type Props = {
pageUrl: string
title: string
image: string
meta: IMeta
@@ -35,7 +32,6 @@ const Page: NextPage<Props> = props => {
return (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
wide={true}
>
@@ -83,7 +79,6 @@ const Page: NextPage<Props> = props => {
export const unstable_getStaticProps = async (ctx: NextPageContext) => {
// const pageUrl = ctx.req.url
const pageUrl = '/collective'
const res = await requestGet<IResponseItems<IPerson>>(createApiUrl('/api/persons'), null)
const data = res?.items || []
@@ -96,11 +91,9 @@ export const unstable_getStaticProps = async (ctx: NextPageContext) => {
props: {
data,
image,
pageUrl,
title,
meta: meta({
title,
url: pageUrl,
description: 'Преподаватели Шлиссельбургской ДХШ',
})
}
-6
View File
@@ -3,8 +3,6 @@ import Head from 'next/head'
import { App } from 'src/components/App'
import Document from 'src/components/Document'
import { Share } from 'src/components/Share'
import menuModel from 'src/models/menu'
import { buildMenu } from 'src/lib/menu'
import { meta } from 'src/lib/meta'
import { Meta } from 'src/components/Meta'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
@@ -12,7 +10,6 @@ import { NextPage } from 'next'
import { IMeta } from 'src/types'
type Props = {
pageUrl: string
data: any //(file)
meta: IMeta
}
@@ -26,7 +23,6 @@ const Page: NextPage<Props> = props => {
return (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
>
<Head>
@@ -40,14 +36,12 @@ const Page: NextPage<Props> = props => {
}
export const unstable_getStaticProps = async (ctx: any) => {
const pageUrl = '/documents'
const id = ctx.params.slug
const data = await requestGet(createApiUrl(`/api/files/${id}`), {})
return {
props: {
data,
pageUrl,
meta: meta({
// title: file.meta,
})
-6
View File
@@ -1,8 +1,6 @@
import Head from 'next/head'
import { App } from 'src/components/App'
import { Meta } from 'src/components/Meta'
import menuModel from 'src/models/menu'
import { buildMenu } from 'src/lib/menu'
import { meta } from 'src/lib/meta'
import { IDocument } from 'src/types'
import { DocumentList } from 'src/components/DocumentList'
@@ -94,14 +92,12 @@ function getSorted(documents: IDocument[]): IDocumentCategory[] {
interface IProps {
collections: IDocumentCategory[]
pageUrl: string
title: string
meta: any
}
const Page: NextPage<IProps> = props => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
>
<Head>
@@ -122,7 +118,6 @@ const Page: NextPage<IProps> = props => (
)
export const unstable_getStaticProps = async (ctx) => {
const pageUrl = '/documents'
const res = await requestGet<IResponseItems<IDocument>>(createApiUrl('/api/files'), null)
const files = res.items
const title = 'Документы'
@@ -131,7 +126,6 @@ export const unstable_getStaticProps = async (ctx) => {
return {
props: {
collections,
pageUrl,
title,
meta: meta({
title,
-6
View File
@@ -1,8 +1,6 @@
import Head from 'next/head'
import { App } from 'src/components/App'
import { Meta } from 'src/components/Meta'
import menuModel from 'src/models/menu'
import { buildMenu } from 'src/lib/menu'
import { splitBy } from 'src/lib/array'
import { meta } from 'src/lib/meta'
import { createApiUrl, requestGet } from 'src/next-lib'
@@ -64,14 +62,12 @@ const ACollection = ({ title, albums }) => (
interface IProps {
meta: any
pageUrl: string
title: string
collections: any[]
}
const Page: NextPage<IProps> = props => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
wide={true}
>
@@ -96,7 +92,6 @@ const Page: NextPage<IProps> = props => (
export const unstable_getStaticProps = async (ctx: any) => {
// const pageUrl = ctx.req.url
const pageUrl = '/gallery'
const res = await requestGet<any>(createApiUrl(`/api/albums`), null)
if (!res) {
return null
@@ -108,7 +103,6 @@ export const unstable_getStaticProps = async (ctx: any) => {
return {
props: {
collections: [...albumCollections.entries()],
pageUrl,
title,
meta: meta({
title,
+1 -7
View File
@@ -3,15 +3,12 @@ import { App } from 'src/components/App'
import { Meta } from 'src/components/Meta'
import { ArticleGrid } from 'src/components/ArticleGrid'
import { HudozkaTitle } from 'src/components/HudozkaTitle'
import menuModel from 'src/models/menu'
import { meta } from 'src/lib/meta'
import { buildMenu } from 'src/lib/menu'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
import { NextPage } from 'next'
import { IArticle } from 'src/types'
interface IProps {
pageUrl: string
title: string
meta: any
prevPage: number
@@ -21,7 +18,6 @@ interface IProps {
const Page: NextPage<IProps> = props => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
wide={true}
>
@@ -45,7 +41,6 @@ const Page: NextPage<IProps> = props => (
)
export const unstable_getStaticProps = async () => {
const pageUrl = '/'
const page = 1
const pageSize = process.env.APP_ARTICLES_PAGE_SIZE
const res = await requestGet<IResponseItems<IArticle>>(createApiUrl(`/api/articles?page=${page}&pageSize=${pageSize}`), null)
@@ -63,11 +58,10 @@ export const unstable_getStaticProps = async () => {
articles,
nextPage,
prevPage,
pageUrl,
title,
meta: meta({
title,
url: pageUrl,
url: '/',
})
}
}
-7
View File
@@ -2,20 +2,16 @@ import Head from 'next/head'
import { App } from 'src/components/App'
import { Schedule } from 'src/components/Schedule'
import { Share } from 'src/components/Share'
import menuModel from 'src/models/menu'
import { buildMenu } from 'src/lib/menu'
import { createApiUrl, requestGet } from 'src/next-lib'
import { NextPage } from 'next'
type Props = {
pageUrl: string
title: string
data: any //(schedule)
}
const Page: NextPage<Props> = props => (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
wide={true}
>
@@ -30,8 +26,6 @@ const Page: NextPage<Props> = props => (
)
export const unstable_getStaticProps = async (ctx: any) => {
// const pageUrl = ctx.req.url
const pageUrl = '/schedule'
const period = '2016-2017'
const semester = 'spring'
const data = await requestGet(createApiUrl(`/api/schedule?period=${period}&semester=${semester}`), null)
@@ -42,7 +36,6 @@ export const unstable_getStaticProps = async (ctx: any) => {
return {
props: {
data,
pageUrl,
title: 'Расписание',
}
}
-6
View File
@@ -2,8 +2,6 @@ import Head from 'next/head'
import { App } from 'src/components/App'
import { Article } from 'src/components/Article'
import { Meta } from 'src/components/Meta'
import menuModel from 'src/models/menu'
import { buildMenu } from 'src/lib/menu'
import { meta } from 'src/lib/meta'
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
import { NextPage } from 'next'
@@ -11,7 +9,6 @@ import { IMeta } from 'src/types'
import { Html } from 'src/components/Html'
interface IProps {
pageUrl: string
title: string
meta: IMeta
person: any
@@ -26,7 +23,6 @@ const Index: NextPage<IProps> = props => {
return (
<App
menu={buildMenu(props.pageUrl, menuModel)}
showAuthor={true}
>
<Head>
@@ -53,7 +49,6 @@ const Index: NextPage<IProps> = props => {
export const unstable_getStaticProps = async (ctx: any) => {
console.log('call unstable_getStaticProps', ctx)
const pageUrl = '/collective'
// const id = ctx.query.person
// const id = ctx.params.person ?? 'va-sarzhin'
const id = ctx.params.person //?? 'mg-timasheva'
@@ -73,7 +68,6 @@ export const unstable_getStaticProps = async (ctx: any) => {
return {
props: {
pageUrl,
person,
title,
meta: meta({
Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 B

-22
View File
@@ -1,22 +0,0 @@
export const Header: React.FC = props => (
<header>
<style jsx>{`
.navigation {
width: 100%;
background-color: white;
}
.navigation__body {
display: flex;
justify-content: space-between;
align-items: center;
}
`}</style>
<div className="navigation">
<div className="navigation__body">
{props.children}
</div>
</div>
</header>
)
+32 -18
View File
@@ -1,36 +1,54 @@
import dynamic from 'next/dynamic'
import cx from 'classnames'
import Comments from '../Comments'
import { useReducedMotion } from 'src/hooks/useReducedMotion'
import { Wrapper } from '../Wrapper'
import { Header } from './Header'
import { ConfigContext } from 'src/context/ConfigContext'
import config from 'src/config'
import { Footer } from '../Footer'
import { Content } from '../Content'
import { Block } from '../Block'
import { Breadcrumbs } from '../Breadcrumbs'
import { IBreadcumbsPart } from 'src/types'
import { useRouter } from 'next/router'
import { useMobile } from 'src/hooks/useMobile'
const Navigation = dynamic(() => import('../Navigation').then(mod => mod.Navigation), {
ssr: false,
})
export interface IAppProps {
menu: any
showAuthor: boolean
wide?: boolean
contentStyle?: React.CSSProperties
breadcrumbs?: IBreadcumbsPart[]
}
export const App: React.FC<IAppProps> = ({ wide = false, ...props }) => {
const motionDisabled = useReducedMotion()
export const App: React.FC<IAppProps> = props => {
const hideBreadcrumbs = useMobile()
const router = useRouter()
const wide = props.wide ?? false
const blockStyle = {
justifyContent: 'center'
}
return (
<ConfigContext.Provider value={config}>
<Wrapper
header={(
<Header>
<Navigation
items={props.menu.items}
<header>
<Navigation style={{
margin: '0 var(--size-m) var(--size-m)',
}} />
{hideBreadcrumbs || !props.breadcrumbs ? null : (
<Block direction={'horizontal'} style={blockStyle}>
<Content wide={wide}>
<Breadcrumbs
items={props.breadcrumbs}
path={router.asPath}
/>
</Header>
</Content>
</Block>
)}
</header>
)}
footer={(
<Footer
@@ -38,15 +56,11 @@ export const App: React.FC<IAppProps> = ({ wide = false, ...props }) => {
/>
)}
>
<div
className={cx('content', {
'reduced-motion': motionDisabled,
'content_wide': wide,
})}
style={props.contentStyle}
>
<Block direction={'horizontal'} style={blockStyle}>
<Content wide={wide} style={props.contentStyle}>
{props.children}
</div>
</Content>
</Block>
{/* <Comments /> */}
</Wrapper>
+42
View File
@@ -0,0 +1,42 @@
import './styles.css'
import { IBreadcumbsPart } from 'src/types'
import { Button } from '../Button'
import { insertBetween } from 'src/lib/array'
export type BreadcrumbsProps = {
style?: React.CSSProperties
items: IBreadcumbsPart[]
path: string
}
export const Breadcrumbs: React.FC<BreadcrumbsProps> = props => {
const items = insertBetween(props.items, () => '/')
return (
<nav className={'breadcrumbs'} style={props.style}>
{items.map((x, i) => {
if (typeof x === 'string') {
return (
<li key={i}>
<span style={{ margin: '0 var(--size-xs)' }}>{x}</span>
</li>
)
}
return (
<li key={x.href}>
<Button
href={x.href}
theme={'ghost'}
size={'small'}
disabled={x.href === props.path}
>
{x.name}
</Button>
</li>
)
})}
</nav >
)
}
+19
View File
@@ -0,0 +1,19 @@
.breadcrumbs {
display: flex;
align-items: center;
flex-wrap: wrap;
list-style: none;
position: relative;
right: var(--size-s);
}
.breadcrumbs li {
margin-bottom: var(--size-xs);
}
.breadcrumbCurrent {
color: var(--color-text-second);
font-size: var(--font-size-second);
text-overflow: ellipsis;
}
+24 -3
View File
@@ -1,16 +1,33 @@
import './styles.css'
import Link from 'next/link'
import cx from 'classnames'
import { useCallback } from 'react'
export type ButtonProps = {
value?: any
style?: React.CSSProperties
href?: string
theme?: 'default' | 'ghost' | 'icon'
size?: 'default' | 'small'
disabled?: boolean
onClick?: (value: any, event: MouseEvent) => void
}
export const Button: React.FC<ButtonProps> = props => {
export const Button: React.FC<ButtonProps> = ({ size = 'default', theme = 'default', disabled = false, ...props }) => {
const className = cx('button', `size_${size}`, `theme_${theme}`, {
disabled
})
const onClick = useCallback(event => {
if (props.onClick) {
props.onClick(props.value, event)
}
}, [props.value, props.onClick])
if (props.href) {
return (
<Link href={props.href}>
<a className={'button invisible'} style={props.style}>
<a className={className} style={props.style}>
{props.children}
</a>
</Link>
@@ -18,7 +35,11 @@ export const Button: React.FC<ButtonProps> = props => {
}
return (
<button className={'button'} style={props.style}>
<button
className={className}
style={props.style}
onClick={onClick}
>
{props.children}
</button>
)
+35 -7
View File
@@ -1,18 +1,13 @@
.button {
border: none;
display: block;
font-size: var(--font-size-default);
text-align: center;
margin-top: var(--size-m);
margin-bottom: var(--size-m);
margin-left: 1px;
margin-right: 1px;
padding: var(--size-s) var(--size-m);
border-radius: 100px;
border: 1px solid var(--color-shadow);
background-color: var(--color-back);
box-shadow: 0 0 0 1px var(--color-shadow);
transition: box-shadow 150ms;
@@ -21,5 +16,38 @@
}
.button:hover {
margin: 1px;
/* margin-left: 1px;
margin-right: 1px; */
border: none;
box-shadow: var(--shadow-active);
}
/* MODIFICATORS */
.button.disabled {
pointer-events: none;
color: var(--color-text-second);
}
/* SIZE */
.button.size_default {
padding: var(--size-s) var(--size-m);
}
.button.size_small {
padding: var(--size-xs) var(--size-s);
font-size: var(--font-size-second);
}
/* THEME */
.button.theme_ghost {
border-color: rgba(0, 0, 0, 0);
}
.button.theme_icon {
padding: var(--size-xs) var(--size-xs);
}
+23
View File
@@ -0,0 +1,23 @@
import cx from 'classnames'
import { useReducedMotion } from 'src/hooks/useReducedMotion'
export type ContentProps = {
style?: React.CSSProperties
wide: boolean
}
export const Content: React.FC<ContentProps> = props => {
const motionDisabled = useReducedMotion()
return (
<div
className={cx('content', {
'reduced-motion': motionDisabled,
'content_wide': props.wide,
})}
style={props.style}
>
{props.children}
</div>
)
}
+15
View File
@@ -0,0 +1,15 @@
import Link from 'next/link'
export interface ActiveLinkProps {
href?: string
activeStyle?: React.CSSProperties
}
export const ActiveLink: React.FC<ActiveLinkProps> = props => props.href
? (
<Link href={props.href}>
<a>{props.children}</a>
</Link>
) : (
<span style={props.activeStyle}>{props.children}</span>
)
+29 -28
View File
@@ -1,33 +1,34 @@
import * as React from 'react'
import cx from 'classnames'
import { useRouter } from 'next/router'
import { ActiveLink } from './ActiveLink'
import { isPartOfPath } from 'src/lib/url'
export interface IMenuItemProps {
active: boolean
text: string
url: string
export type MenuItemProps = {
href: string
layout: 'desktop' | 'mobile'
}
export const MenuItem: React.FC<IMenuItemProps> = ({ active, text, url }) => (
<>
<style jsx>{`
a {
color: var(--dark-color);
}
export const MenuItem: React.FC<MenuItemProps> = props => {
const ignore = ['/']
const router = useRouter()
const current = router.asPath === props.href
const selected = current || (!ignore.includes(props.href) && isPartOfPath(props.href, router.asPath))
const href = current ? null : props.href
a:visited {
color: var(--dark-color);
}
a:hover {
border-color: var(--dark-color);
}
`}</style>
{active
? (
<span>{text}</span>
) : (
<a href={url}>{text}</a>
return (
<li
className={cx('menuItem', props.layout, {
selected,
})}
>
<ActiveLink
href={href}
activeStyle={{
fontWeight: 'bold'
}}
>
{props.children}
</ActiveLink>
</li>
)
}
</>
)
}
+13 -115
View File
@@ -1,125 +1,23 @@
import * as React from 'react'
import './styles.css'
import cx from 'classnames'
import { getPathWithNoTrailingSlash } from '../../lib/url'
import { MenuItem } from './MenuItem'
import { IMenu } from 'src/types'
const itemUrl = (url: string) => getPathWithNoTrailingSlash(url)
export interface IMenuProps {
export type MenuProps = {
layout: 'desktop' | 'mobile'
items: any[]
items: IMenu[]
}
export const Menu: React.FC<IMenuProps> = props => (
<div className={cx('menu', props.layout)}>
<style jsx>{`
.menu {
display: flex;
align-items: flex-start;
list-style: none;
}
div.desktop {
flex-direction: row;
flex-wrap: wrap;
}
div.mobile {
flex-direction: column;
}
li{
position: relative;
border-top: var(--menu-colored-mark-thickness) solid rgba(0, 0, 0, 0);
}
li.desktop {
padding: 0.3em 0.2em 0;
margin-right: 1em;
font-size: var(--normal-font-size);
}
li.mobile {
padding: 0.3em 0.5em;
margin-bottom: var(--half-margin);
font-size: 1.25em;
color: white;
}
li:last-child {
margin-right: 0;
}
li.selected {
font-weight: bold;
}
li.desktop.selected.blue {
border-top-color: var(--blue-color);
}
li.desktop.selected.orange {
border-top-color: var(--orange-color);
}
li.desktop.selected.green {
border-top-color: var(--green-color);
}
li.desktop.selected.yellow {
border-top-color: var(--yellow-color);
}
li.desktop.selected.pink {
border-top-color: var(--pink-color);
}
li.desktop.selected.red {
border-top-color: var(--red-color);
}
li.mobile.selected.blue {
background-color: var(--blue-color);
}
li.mobile.selected.orange {
background-color: var(--orange-color);
}
li.mobile.selected.green {
background-color: var(--green-color);
}
li.mobile.selected.yellow {
background-color: var(--yellow-color);
}
li.mobile.selected.pink {
background-color: var(--pink-color);
}
li.mobile.selected.red {
background-color: var(--red-color);
}
`}</style>
export const Menu: React.FC<MenuProps> = props => (
<menu className={cx('menu', props.layout)}>
{props.items.map((item, index) => (
<li key={index}
className={cx(
props.layout,
item.color, {
selected: item.highlighted,
})}
<MenuItem key={index}
href={item.href}
layout={props.layout}
>
<MenuItem
url={itemUrl(item.url)}
text={item.text}
active={item.active}
/>
</li>
{item.name}
</MenuItem>
))}
</div>
</menu>
)
+48
View File
@@ -0,0 +1,48 @@
.menu {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
font-size: var(--font-size-second);
list-style: none;
}
.menu.desktop {
flex-direction: row;
}
.menu.mobile {
flex-direction: column;
}
.menuItem {
position: relative;
border-top: var(--menu-mark-thickness) solid rgba(0, 0, 0, 0);
}
/* MOBILE */
.menuItem.mobile {
font-size: var(--font-size-default);
padding: var(--size-xs) var(--size-s);
}
.menuItem.mobile.selected {
background-color: var(--color-back-opposite);
color: var(--color-text-opposite)
}
.menuItem.mobile.selected a {
color: var(--color-text-opposite)
}
/* DESKTOP */
.menuItem.desktop {
padding: var(--size-xs) var(--size-s);
}
.menuItem.desktop.selected {
border-top-color: var(--color-back-opposite);
}
+11 -63
View File
@@ -1,79 +1,27 @@
import * as React from 'react'
import './styles.css'
import cx from 'classnames'
import { Button } from '../Button'
export interface IMenuToggleProps {
style?: React.CSSProperties
open: boolean
position: 'left' | 'right'
onClick: () => void
}
export const MenuToggle: React.FC<IMenuToggleProps> = props => {
return (
<Button
onClick={props.onClick}
style={props.style}
theme={'icon'}
>
<div
className={cx(props.position, {
className={cx('toggle', {
open: props.open,
close: !props.open,
})}
style={props.style}
>
<style jsx>{`
div {
--hmenu-margin: 6px;
--hmenu-icon-width: 32px;
--hmenu-icon-height: 32px;
position: absolute;
height: var(--hmenu-icon-height);
margin: var(--hmenu-margin);
}
div.right {
top: 0;
right: 0;
}
div.left {
top: 0;
left: 0;
}
a {
display: block;
text-decoration: none;
border: none;
width: var(--hmenu-icon-width);
height: var(--hmenu-icon-height);
background-size: 100%;
}
a:hover {
text-decoration: none;
border: none;
}
div.open a {
background-image: url(/static/graphics/hmenu-close-b.png);
}
div.open a:hover {
background-image: url(/static/graphics/hmenu-close-hover-b.png);
}
div.close a {
background-image: url(/static/graphics/hmenu-b.png);
}
div.close a:hover {
background-image: url(/static/graphics/hmenu-hover-b.png);
}
`}</style>
<a
href="#"
onClick={props.onClick}
></a>
</div>
/>
</Button>
)
}
+14
View File
@@ -0,0 +1,14 @@
.toggle {
--s: 32px;
width: var(--s);
height: var(--s);
background-size: var(--s);
}
.toggle.open {
background-image: url(/static/graphics/hmenu-close-b.png);
}
.toggle.close {
background-image: url(/static/graphics/hmenu-b.png);
}
+50 -41
View File
@@ -1,67 +1,76 @@
import * as React from 'react'
import { MenuToggle } from '../MenuToggle'
import { useScreenType } from '../../hooks/useScreenType'
import { useMobile } from 'src/hooks/useMobile'
import { Menu } from '../Menu'
import { Overlay } from '../Overlay'
import { useState, useCallback, useContext } from 'react'
import { ConfigContext } from 'src/context/ConfigContext'
import { Block } from '../Block'
import { Spacer } from '../Spacer'
export interface INavigationProps {
items: any[]
style?: React.CSSProperties
}
export const Navigation: React.FC<INavigationProps> = props => {
const isMobile = useScreenType(['phone', 'tablet'])
const [open, setOpen] = React.useState(false)
const onClick = React.useCallback(
const collapseMenu = useMobile()
const [open, setOpen] = useState(false)
const onClick = useCallback(
() => {
setOpen(!open)
},
[open]
)
const { menu } = useContext(ConfigContext)
return (
<nav>
<style jsx>{`
nav {
display: flex;
align-items: flex-start;
margin-left: 1em;
margin-right: 1em;
font-size: var(--normal-font-size);
list-style: none;
}
`}</style>
{isMobile ? (
<MenuToggle
open={open}
position={'right'}
onClick={onClick}
style={{
zIndex: 1000001,
}}
/>
) : (
<Menu
layout={'desktop'}
items={props.items}
/>
)}
<>
<Overlay
show={open}
onClickOverlay={onClick}
>
<div style={{
width: '50%',
margin: '50px auto',
<Block direction={'horizontal'}>
<Spacer />
<MenuToggle
open={open}
onClick={onClick}
style={{
margin: 'var(--size-xs)'
}}
/>
</Block>
<Block direction={'horizontal'} style={{
justifyContent: 'center',
}}>
<nav style={{
width: '75%',
}}>
<Menu
layout={'mobile'}
items={props.items}
items={menu}
/>
</div>
</Overlay>
</nav>
</Block>
</Overlay>
{collapseMenu ? (
<Block direction={'horizontal'}>
<Spacer />
<MenuToggle
open={open}
onClick={onClick}
style={{
margin: 'var(--size-xs)'
}}
/>
</Block>
) : (
<nav style={props.style}>
<Menu
layout={'desktop'}
items={menu}
/>
</nav>
)}
</>
)
}
+16 -20
View File
@@ -1,46 +1,42 @@
import * as React from 'react'
import './styles.css'
import { Transition } from './Transition'
import { useReducedMotion } from '../../hooks/useReducedMotion'
import { useCallback, useEffect } from 'react'
export interface IOverlayProps {
show: boolean
onClickOverlay: () => void
style?: React.CSSProperties
}
export const Overlay: React.FC<IOverlayProps> = props => {
const motionDisabled = useReducedMotion()
const duration = motionDisabled ? 0 : 250
const onClick = React.useCallback((event: React.MouseEvent<HTMLDivElement>) => {
const onClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (event.target === event.currentTarget) {
props.onClickOverlay()
}
}, [props.onClickOverlay])
const duration = motionDisabled ? 0 : 250
useEffect(() => {
if (props.show) {
document.body.classList.add('noscroll')
} else {
document.body.classList.remove('noscroll')
}
}, [props.show])
return (
<Transition
duration={duration}
show={props.show}
extraStyle={{
zIndex: 1000000,
}}
>
<div
onClick={onClick}
className={'overlay'}
style={props.style}
>
<style jsx>{`
div {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow-y: auto;
background-color: rgb(255, 255, 255);
}
`}</style>
{props.children}
</div>
</Transition>
+11
View File
@@ -0,0 +1,11 @@
.overlay {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow-y: auto;
z-index: 1000000;
background-color: var(--color-back-main);
}
-1
View File
@@ -1,4 +1,3 @@
import * as React from 'react'
import { Share } from '../Share'
import { Html } from '../Html'
+4 -11
View File
@@ -3,10 +3,12 @@ import cx from 'classnames'
export type WrapperProps = {
header: React.ReactNode
footer: React.ReactNode
style?: React.CSSProperties
mainStyle?: React.CSSProperties
}
export const Wrapper: React.FC<WrapperProps> = props => (
<div className={cx('wrapper', 'theme-default')}>
<div className={cx('wrapper', 'theme-default')} style={props.style}>
<style jsx>{`
.wrapper {
display: flex;
@@ -17,20 +19,11 @@ export const Wrapper: React.FC<WrapperProps> = props => (
font-family: var(--text-font);
font-size: var(--font-size-default);
}
.content {
flex: 1;
}
main {
display: flex;
justify-content: center;
}
`}</style>
{props.header}
<main>
<main style={props.mainStyle}>
{props.children}
</main>
+38
View File
@@ -29,5 +29,43 @@ export default {
value: 'https://www.instagram.com/hudozka',
title: 'Инстаграм'
}
],
menu: [
{
name: 'Школа',
href: '/',
},
{
name: 'Расписание',
href: '/schedule',
},
{
name: 'Поступление',
href: '/join',
},
{
name: 'Предметы',
href: '/courses',
},
{
name: 'Коллектив',
href: '/collective',
},
{
name: 'Галерея',
href: '/gallery',
},
{
name: '«Мы соседи»',
href: '/neighbors',
},
{
name: 'Документы',
href: '/documents',
},
{
name: 'Сведения об образовательной организации',
href: '/info/',
},
]
}
+5
View File
@@ -0,0 +1,5 @@
import useMedia from 'use-media'
export function useMobile(): boolean {
return useMedia('(max-width: 31.25em)', false)
}
-15
View File
@@ -1,15 +0,0 @@
import * as React from 'react'
import useMedia from 'use-media'
export type Type = 'phone' | 'tablet' | 'laptop' | 'desktop'
export function useScreenType(types: Type[]): boolean {
const isPhone = useMedia('(max-width: 31.25em)')
const [match, setMatch] = React.useState(false)
React.useEffect(() => {
setMatch(isPhone && types.includes('phone'))
}, [isPhone])
return match
}
-26
View File
@@ -1,26 +0,0 @@
export function splitBy(fn) {
return items => {
return items.reduce((acc, item) => {
const key = fn(item)
const items = acc.get(key) || []
return acc.set(key, [...items, item])
}, new Map())
}
}
export function head(list) {
return Array.isArray(list) && list.length
? list[0]
: null
}
export function last(list) {
return Array.isArray(list) && list.length
? list[list.length - 1]
: null
}
// exports.splitBy = splitBy
// exports.head = head
// exports.last = last
+22
View File
@@ -0,0 +1,22 @@
export function splitBy(fn) {
return items => {
return items.reduce((acc, item) => {
const key = fn(item)
const items = acc.get(key) || []
return acc.set(key, [...items, item])
}, new Map())
}
}
export function insertBetween<T, I = T>(items: T[], fn: (i: number) => I): Array<T | I> {
return items.reduce((acc, x, i) => {
acc.push(x)
if (i < items.length - 1) {
acc.push(fn(i))
}
return acc
}, [])
}
-50
View File
@@ -1,50 +0,0 @@
import url from 'url'
import { getPathWithNoTrailingSlash } from '../lib/url'
import { urlToPattern } from '../lib/url'
import { isMatchPathPattern } from '../lib/url'
import { isEqualPath } from '../lib/url'
import { compose, any } from '../lib/common'
function isActive(path, menuItem) {
return isEqualPath(path, menuItem.url, true)
}
function isHighlighted(path, menuItem) {
return any(flatMenuItem(menuItem)
.map(i => isMatchPathPattern(
compose(urlToPattern, getPathWithNoTrailingSlash)(i.url),
path,
true
))
)
}
function flatMenuItem(item) {
const rootUrl = item.url
const flatUrls = items => {
return items.map(item => ({
...item,
url: url.resolve(rootUrl, item.url)
}))
}
return !item.items
? [item]
: item.items.reduce((acc, i) => [...acc, ...compose(flatUrls, flatMenuItem)(i)], [item])
}
/**
* Takes plain menu model and sample path and defines is an item of menu should be active
*
* @param path
* @param menu
* @return {{items}}
*/
export function buildMenu(path, menu) {
const items = menu.map(item => ({
...item,
active: isActive(path, item),
highlighted: isHighlighted(path, item),
}))
return { items }
}
-40
View File
@@ -1,40 +0,0 @@
function getPathWithNoTrailingSlash(path='') {
path = path.charAt(path.length - 1) === '/'
? path.substr(0, path.length - 1)
: path
return path.length === 0
? '/'
: path
}
function isEqualPath(a, b, ignoreTrailingSlash = false) {
return ignoreTrailingSlash
? getPathWithNoTrailingSlash(a) === getPathWithNoTrailingSlash(b)
: a === b
}
function isMatchPathPattern(pattern, path, ignoreTrailingSlash = false) {
const test = ignoreTrailingSlash
? getPathWithNoTrailingSlash(path)
: path
return pattern.test(test)
}
/**
*
* /article/* -> ^\/article\/[/\w\d-_]+\/?$
*
* @param url
* @return {RegExp}
*/
function urlToPattern(url) {
const u = url.replace('/*', '[/\\w\\d-_]+')
const pattern = `^${u}/?$`
return new RegExp(pattern)
}
exports.getPathWithNoTrailingSlash = getPathWithNoTrailingSlash
exports.isMatchPathPattern = isMatchPathPattern
exports.urlToPattern = urlToPattern
exports.isEqualPath = isEqualPath
+3
View File
@@ -0,0 +1,3 @@
export function isPartOfPath(part: string, path: string): boolean {
return path.startsWith(part)
}
-102
View File
@@ -1,102 +0,0 @@
export default [
{
url: '/',
text: 'Школа',
color: 'blue',
items: [
{url: '/summer-school/2017', text: 'Летние каникулы 2017'},
{url: '/articles/*', text: 'Статьи'},
{url: '/article/*', text: ''},
],
},
{
text: 'Расписание',
url: '/schedule/',
color: 'green',
items: [
{url: '*', text: ''},
]
},
{
'text': 'Поступление',
'url': '/join',
'color': 'yellow'
},
{
url: '/courses/',
text: 'Предметы',
color: 'magenta',
items: [
{url: 'ceramics', text: 'Керамика'},
{url: 'batik', text: 'Живопись'},
{url: 'easel-composition', text: 'Живопись'},
{url: 'cg', text: 'Живопись'},
{url: 'scale-modeling', text: 'Живопись'},
{url: 'drawing', text: 'Живопись'},
{url: 'costume-composition', text: 'Живопись'},
{url: 'felting', text: 'Живопись'},
{url: 'art-history', text: 'Живопись'},
{url: 'perishki', text: 'Живопись'},
{url: 'matreshki', text: 'Живопись'},
{url: 'listochki', text: 'Живопись'},
]
},
{
text: 'Коллектив',
url: '/collective',
color: 'pink',
items: [
{url: '/teacher/*', text: ''},
]
},
{
text: 'Галерея',
url: '/gallery',
color: 'red',
items: [
{url: '/album/*', text: ''},
]
},
{
text: '«Мы соседи»',
url: '/neighbors',
color: 'yellow',
},
{
text: 'Документы',
url: '/documents',
color: 'gray',
items: [
{url: '/document/*', text: ''},
]
},
{
url: '/info/',
text: 'Сведения об образовательной организации',
color: 'orange',
items: [
{url: 'main', text: 'Основные сведения'},
{url: 'structure', text: 'Структура и органы управления образовательной организацией'},
{url: 'education', text: 'Образование'},
{url: 'standards', text: 'Образовательные стандарты'},
//{url: '/collective', text: 'Руководство. Педагогический (научно-педагогический) состав'},
{url: 'support', text: 'Материально-техническое обеспечение образовательного процесса'},
{url: 'grants', text: 'Стипендии и иные виды материальной поддержки'},
{url: 'paid-services', text: 'Платные образовательные услуги'},
{url: 'finances', text: 'Финансово-хозяйственная деятельность'},
{url: 'vacancy', text: 'Вакантные места для приема'},
{url: 'tools', text: 'Список рекомендованных материалов'},
{url: 'history', text: 'История школы'},
{url: '*', text: ''},
]
},
{
'text': 'Текущий контроль',
'url': '/info/audit',
'color': 'gray'
}
]
+9 -13
View File
@@ -12,7 +12,7 @@
--biggest-font-size: 2.25rem;
/* MENU */
--menu-colored-mark-thickness: 4px;
--menu-mark-thickness: 4px;
/* SIZE */
--mobile-width: 31.25em;
@@ -59,16 +59,16 @@
--schedule-row-color: hsl(210, 55%, 90%);
/* TEXT COLOR */
--color-text: #303030;
--color-text: #202020;
--color-text-second: #9090a0;
--color-text-opposite: var(--light-color);
--color-text-opposite: #f0f0f0;
--color-back-main: white;
--color-back-opposite: var(--darker-color);
/* SIZE */
--size-xs: 0.25em;
--size-s: 0.5em;
--size-xs: 0.35em;
--size-s: 0.75em;
--size-m: 1em;
--size-l: 2em;
--size-xl: 3em;
@@ -175,12 +175,8 @@ a.invisible {
border-bottom-color: var(--color-text-opposite);
}
.actor {
border-bottom: 1px dashed black;
}
.actor:hover {
border-bottom: 2px solid black;
.noscroll {
overflow: hidden;
}
@media (max-width: 31.25em) {
@@ -196,8 +192,8 @@ a.invisible {
--font-size-h3: 1.25rem;
--content-width: 100%;
--content-margin: var(--size-l);
--content-wide-margin: var(--size-l) var(--size-l);
--content-margin: var(--size-m);
--content-wide-margin: var(--size-m) var(--size-m);
--kazimir-image-increase: 0px;
}
+20
View File
@@ -1,3 +1,23 @@
export interface IMenu {
name: string
href: string
}
export interface IBreadcumbsPart {
name: string
href: string
}
export interface IPage {
id: string
title: string
url: string
data: string
preview: any //IImage
tokens: any[]
breadcrumb?: IBreadcumbsPart[]
}
export interface IDocument {
category: string
fileName: string