42 files changed
+913
-579
No files matched your search
+2
-1
@@ -14,11 +14,12 @@
|
||||
"warn",
|
||||
"double"
|
||||
],
|
||||
"no-tabs": "off",
|
||||
"no-tabs": "error",
|
||||
"indent": [
|
||||
"warn",
|
||||
4
|
||||
],
|
||||
"no-var": "error",
|
||||
"eol-last": ["error", "always"],
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error"
|
||||
|
||||
Generated
+591
-191
File diff suppressed because it is too large.
Load diff
+6
-4
@@ -18,6 +18,8 @@
|
||||
"author": "Roman Timashev",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@radix-ui/react-navigation-menu": "^1.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"date-fns": "^2.11.1",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -25,10 +27,10 @@
|
||||
"next": "^12.2.5",
|
||||
"next-seo": "^5.5.0",
|
||||
"rc-tooltip": "^5.1.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-use": "^17.2.1",
|
||||
"react-use": "^17.4.0",
|
||||
"sharp": "^0.31.0",
|
||||
"sitemap": "^1.5.0",
|
||||
"typograf": "^6.11.1",
|
||||
@@ -39,7 +41,7 @@
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@types/node": "^12.12.34",
|
||||
"@types/react": "^16.9.32",
|
||||
"@types/react": "^18.0.18",
|
||||
"@types/sharp": "^0.29.2",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-next": "12.2.5",
|
||||
|
||||
+9
-9
@@ -1,25 +1,25 @@
|
||||
import { NextPage } from "next"
|
||||
import { Wrapper } from "src/components/Wrapper"
|
||||
import { NotFound } from "src/components/NotFound"
|
||||
import { Copyright } from "src/components/Copyright"
|
||||
import { Block } from "src/components/Block"
|
||||
import { Spacer } from "src/components/Spacer"
|
||||
import { Wrapper } from "@/components/Wrapper"
|
||||
import { NotFound } from "@/components/NotFound"
|
||||
import { Copyright } from "@/components/Copyright"
|
||||
import { Spacer } from "@/components/Spacer"
|
||||
import { Box } from "@/ui/Box"
|
||||
|
||||
const Index: NextPage = () => (
|
||||
<Wrapper
|
||||
header={null}
|
||||
footer={(
|
||||
<Block as={"footer"} direction={"horizontal"}>
|
||||
<Box as={"footer"}>
|
||||
<Spacer />
|
||||
<Copyright />
|
||||
<Spacer />
|
||||
</Block>
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Block direction={"vertical"}>
|
||||
<Box vertical>
|
||||
<NotFound />
|
||||
<Spacer />
|
||||
</Block>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 137 B |
Binary file not shown.
|
Before Width: | Height: | Size: 500 B |
@@ -1,29 +1,25 @@
|
||||
import dynamic from "next/dynamic"
|
||||
import { Wrapper } from "../Wrapper"
|
||||
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, IMenu } from "src/types"
|
||||
import { useRouter } from "next/router"
|
||||
import { useMobile } from "src/hooks/useMobile"
|
||||
import { Gosuslugi } from "../Gosuslugi"
|
||||
import { INavigationProps } from "@/components/Navigation"
|
||||
import { Navigation } from "@/components/Navigation"
|
||||
import { Box } from "@/ui/Box"
|
||||
|
||||
const Navigation = dynamic<INavigationProps>(() => import("../Navigation").then(mod => mod.Navigation), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
export interface IAppProps {
|
||||
export type AppProps = {
|
||||
showAuthor?: boolean
|
||||
contentStyle?: React.CSSProperties
|
||||
breadcrumbs?: IBreadcumbsPart[]
|
||||
menu: IMenu[]
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const App: React.FC<IAppProps> = ({ showAuthor = false, menu, ...props }) => {
|
||||
export const App: React.FC<AppProps> = ({ showAuthor = false, menu, ...props }) => {
|
||||
const hideBreadcrumbs = useMobile()
|
||||
const router = useRouter()
|
||||
const blockStyle = {
|
||||
@@ -39,17 +35,17 @@ export const App: React.FC<IAppProps> = ({ showAuthor = false, menu, ...props })
|
||||
header={(
|
||||
<header>
|
||||
<Navigation style={{
|
||||
margin: "0 var(--size-m) var(--size-m)",
|
||||
// margin: "0 var(--size-m) var(--size-m)",
|
||||
}} />
|
||||
{hideBreadcrumbs || !props.breadcrumbs ? null : (
|
||||
<Block direction={"horizontal"} style={blockStyle}>
|
||||
<Box style={blockStyle}>
|
||||
<Content>
|
||||
<Breadcrumbs
|
||||
items={props.breadcrumbs}
|
||||
path={router.asPath}
|
||||
/>
|
||||
</Content>
|
||||
</Block>
|
||||
</Box>
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
@@ -68,11 +64,11 @@ export const App: React.FC<IAppProps> = ({ showAuthor = false, menu, ...props })
|
||||
</Footer>
|
||||
)}
|
||||
>
|
||||
<Block direction={"horizontal"} style={blockStyle}>
|
||||
<Box style={blockStyle}>
|
||||
<Content style={props.contentStyle}>
|
||||
{props.children}
|
||||
</Content>
|
||||
</Block>
|
||||
</Box>
|
||||
</Wrapper>
|
||||
</ConfigContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import s from "./styles.module.css"
|
||||
|
||||
import cx from "classnames"
|
||||
import { createElement } from "react"
|
||||
|
||||
export type BlockProps = {
|
||||
as?: keyof HTMLElementTagNameMap
|
||||
style?: React.CSSProperties
|
||||
direction: "horizontal" | "vertical"
|
||||
}
|
||||
|
||||
export const Block: React.FC<BlockProps> = ({ as = "div", style, direction, ...props }) => {
|
||||
const directionClass = direction === "horizontal"
|
||||
? s.horizontal
|
||||
: s.vertical
|
||||
const newProps = {
|
||||
className: cx(s.block, directionClass),
|
||||
style,
|
||||
}
|
||||
|
||||
return createElement(as, newProps, props.children)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
.block {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import s from "./styles.module.css"
|
||||
|
||||
import { IBreadcumbsPart } from "src/types"
|
||||
import { Button } from "../Button"
|
||||
import { Button } from "@/ui/Button"
|
||||
import { insertBetween } from "src/lib/array"
|
||||
|
||||
export type BreadcrumbsProps = {
|
||||
|
||||
@@ -10,6 +10,7 @@ const layoutClass = {
|
||||
|
||||
export type CardLayout = "simple" | "featured"
|
||||
export type CardProps = {
|
||||
children?: React.ReactNode
|
||||
style?: React.CSSProperties
|
||||
href: string
|
||||
cover: React.ReactNode
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.image {
|
||||
@@ -40,7 +42,7 @@
|
||||
|
||||
.simple .body {
|
||||
position: relative;
|
||||
padding-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.simple .body:before {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import s from "./styles.module.css"
|
||||
|
||||
export type CardGridProps = {
|
||||
children?: React.ReactNode
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useReducedMotion } from "src/hooks/useReducedMotion"
|
||||
|
||||
export type ContentProps = {
|
||||
style?: React.CSSProperties
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Content: React.FC<ContentProps> = props => {
|
||||
|
||||
@@ -8,7 +8,7 @@ export type FileProps = FileDefinition & {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export const File: React.SFC<FileProps> = props => (
|
||||
export const File: React.FC<FileProps> = props => (
|
||||
<div className={s.file} style={props.style}>
|
||||
{!props.cover?.src ? null : (
|
||||
<div className={s.image}>
|
||||
|
||||
@@ -3,34 +3,35 @@ import s from "./footer.module.css"
|
||||
import cx from "classnames"
|
||||
import { Copyright } from "../Copyright"
|
||||
import { Author } from "./Author"
|
||||
import { Block } from "../Block"
|
||||
import { Spacer } from "../Spacer"
|
||||
import { Contacts } from "../Contacts"
|
||||
import { Box } from "@/ui/Box"
|
||||
|
||||
export interface IFooterProps {
|
||||
export type FooterProps = {
|
||||
showAuthor: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Footer: React.FC<IFooterProps> = props => (
|
||||
export const Footer: React.FC<FooterProps> = ({ showAuthor, children }) => (
|
||||
<footer className={cx(s.footer, "opposite")}>
|
||||
<Block direction={"vertical"} style={{
|
||||
<Box vertical align={false} style={{
|
||||
marginBottom: "var(--size-l)",
|
||||
}}>
|
||||
<Contacts />
|
||||
</Block>
|
||||
</Box>
|
||||
|
||||
{props.children}
|
||||
{children}
|
||||
|
||||
<Block direction={"horizontal"}>
|
||||
<Box>
|
||||
<Copyright />
|
||||
|
||||
{!props.showAuthor ? null : (
|
||||
{!showAuthor ? null : (
|
||||
<>
|
||||
<Spacer as={"span"} />
|
||||
<Author />
|
||||
</>
|
||||
)}
|
||||
</Block>
|
||||
</Box>
|
||||
</footer >
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Link from "next/link"
|
||||
|
||||
export interface ActiveLinkProps {
|
||||
export type ActiveLinkProps = {
|
||||
href?: string
|
||||
activeStyle?: React.CSSProperties
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const ActiveLink: React.FC<ActiveLinkProps> = props => props.href
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import s from "./styles.module.css"
|
||||
import cx from "classnames"
|
||||
import { useRouter } from "next/router"
|
||||
import { ActiveLink } from "./ActiveLink"
|
||||
import { isPartOfPath } from "src/lib/url"
|
||||
|
||||
export type MenuItemProps = {
|
||||
href: string
|
||||
}
|
||||
|
||||
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 ? undefined : props.href
|
||||
|
||||
return (
|
||||
<li
|
||||
className={cx({
|
||||
[s.selected]: selected,
|
||||
})}
|
||||
>
|
||||
<ActiveLink
|
||||
href={href}
|
||||
activeStyle={{
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</ActiveLink>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,43 @@
|
||||
import s from "./styles.module.css"
|
||||
|
||||
import cx from "classnames"
|
||||
import { MenuItem } from "./MenuItem"
|
||||
import { IMenu } from "src/types"
|
||||
import { IMenu } from "@/types"
|
||||
import * as NavigationMenu from "@radix-ui/react-navigation-menu"
|
||||
import classnames from "classnames/bind"
|
||||
import { useRouter } from "next/router"
|
||||
import { ActiveLink } from "./ActiveLink"
|
||||
|
||||
const layoutClass = {
|
||||
desktop: s.desktop,
|
||||
mobile: s.mobile,
|
||||
}
|
||||
let cx = classnames.bind(s)
|
||||
|
||||
export type MenuProps = {
|
||||
layout: "desktop" | "mobile"
|
||||
vertical?: boolean
|
||||
items: IMenu[]
|
||||
}
|
||||
|
||||
export const Menu: React.FC<MenuProps> = props => (
|
||||
<menu className={cx(s.menu, layoutClass[props.layout])}>
|
||||
{props.items.map((item, index) => (
|
||||
<MenuItem key={index}
|
||||
href={item.href}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</menu>
|
||||
)
|
||||
export const Menu: React.FC<MenuProps> = ({ vertical = false, items }) => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<NavigationMenu.Root>
|
||||
<NavigationMenu.List className={cx(s.menu, { vertical })}>
|
||||
{
|
||||
items.map(item => {
|
||||
const current = router.asPath === item.href
|
||||
const href = current ? undefined : item.href
|
||||
|
||||
if (!href) {
|
||||
return (
|
||||
<NavigationMenu.Item key={item.href} className={`${s.menuItem} ${s.current}`}>
|
||||
<ActiveLink>{item.name}</ActiveLink>
|
||||
</NavigationMenu.Item>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<NavigationMenu.Item key={item.href} className={`${s.menuItem} ${s.active}`}>
|
||||
<ActiveLink href={item.href}>{item.name}</ActiveLink>
|
||||
</NavigationMenu.Item>
|
||||
)
|
||||
})
|
||||
}
|
||||
</NavigationMenu.List>
|
||||
</NavigationMenu.Root>
|
||||
)
|
||||
}
|
||||
@@ -3,50 +3,51 @@
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: var(--size-m);
|
||||
|
||||
font-size: var(--font-size-second);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* MENU LAYOUT */
|
||||
|
||||
.desktop {
|
||||
/* NOT VERTICAL */
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* MENU ITEM */
|
||||
|
||||
.menu li {
|
||||
position: relative;
|
||||
border-top: var(--menu-mark-thickness) solid rgba(0, 0, 0, 0);
|
||||
.menuItem {
|
||||
user-select: none;
|
||||
background-color: var(--color-back-main);
|
||||
border-radius: var(--size-s);
|
||||
transition: box-shadow 200ms ease-in, background-color 200ms ease-in;
|
||||
padding: var(--size-s) var(--size-s);
|
||||
}
|
||||
|
||||
/* MENU ITEM MOBILE */
|
||||
|
||||
.mobile li {
|
||||
font-size: var(--font-size-default);
|
||||
|
||||
padding: var(--size-xs) var(--size-s);
|
||||
.menuItem.active {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile li.selected {
|
||||
.menuItem.active:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.menuItem.current {
|
||||
color: var(--color-text-opposite);
|
||||
background-color: var(--color-back-opposite);
|
||||
color: var(--color-text-opposite)
|
||||
}
|
||||
|
||||
.mobile li.selected a {
|
||||
color: var(--color-text-opposite)
|
||||
}
|
||||
|
||||
/* MENU ITEM DESKTOP */
|
||||
|
||||
.desktop li {
|
||||
padding: var(--size-xs) var(--size-s);
|
||||
.menuItem a {
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.desktop li.selected {
|
||||
border-top-color: var(--color-back-opposite);
|
||||
/* DARK THEME */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.menuItem.active:hover {
|
||||
box-shadow: none;
|
||||
background-color: var(--color-back);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,22 @@
|
||||
import s from "./styles.module.css"
|
||||
import { Button, ButtonProps } from "@/ui/Button"
|
||||
import { HamburgerMenuIcon, Cross1Icon } from "@radix-ui/react-icons"
|
||||
|
||||
import cx from "classnames"
|
||||
import { Button } from "../Button"
|
||||
|
||||
export interface IMenuToggleProps {
|
||||
style?: React.CSSProperties
|
||||
export type MenuToggleProps = Omit<ButtonProps, "theme"> & {
|
||||
open: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export const MenuToggle: React.FC<IMenuToggleProps> = props => {
|
||||
export const MenuToggle: React.FC<MenuToggleProps> = ({ open, ...props }) => {
|
||||
const icon = open ? (
|
||||
<Cross1Icon width={24} height={24} />
|
||||
) : (
|
||||
<HamburgerMenuIcon width={24} height={24} />
|
||||
)
|
||||
return (
|
||||
<Button
|
||||
onClick={props.onClick}
|
||||
style={props.style}
|
||||
{...props}
|
||||
theme={"icon"}
|
||||
>
|
||||
<div
|
||||
className={cx(s.toggle, {
|
||||
[s.open]: props.open,
|
||||
[s.close]: !props.open,
|
||||
})}
|
||||
/>
|
||||
{icon}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
@@ -1,76 +1,65 @@
|
||||
import { MenuToggle } from "../MenuToggle"
|
||||
import { useMobile } from "src/hooks/useMobile"
|
||||
import { Menu } from "../Menu"
|
||||
import { Overlay } from "../Overlay"
|
||||
import { useState, useCallback, useContext } from "react"
|
||||
import { Overlay } from "@/ui/Overlay"
|
||||
import { useContext } from "react"
|
||||
import { ConfigContext } from "src/context/ConfigContext"
|
||||
import { Block } from "../Block"
|
||||
import { Box } from "@/ui/Box"
|
||||
import { Spacer } from "../Spacer"
|
||||
import { useToggle } from "react-use"
|
||||
|
||||
export interface INavigationProps {
|
||||
export type NavigationProps = {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export const Navigation: React.FC<INavigationProps> = props => {
|
||||
export const Navigation: React.FC<NavigationProps> = props => {
|
||||
const collapseMenu = useMobile()
|
||||
const [open, setOpen] = useState(false)
|
||||
const onClick = useCallback(
|
||||
() => {
|
||||
setOpen(!open)
|
||||
},
|
||||
[open]
|
||||
)
|
||||
const [open, toggleOpen] = useToggle(false)
|
||||
const { menu } = useContext(ConfigContext)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Overlay
|
||||
show={open}
|
||||
onClickOverlay={onClick}
|
||||
onClickOverlay={toggleOpen}
|
||||
duration={250}
|
||||
>
|
||||
<Block direction={"horizontal"}>
|
||||
<Box style={{
|
||||
padding: "var(--size-s)",
|
||||
}}>
|
||||
<Spacer />
|
||||
<MenuToggle
|
||||
open={open}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
margin: "var(--size-xs)",
|
||||
}}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
</Block>
|
||||
<Block direction={"horizontal"} style={{
|
||||
</Box>
|
||||
<Box style={{
|
||||
justifyContent: "center",
|
||||
}}>
|
||||
<nav style={{
|
||||
width: "75%",
|
||||
}}>
|
||||
<Menu
|
||||
layout={"mobile"}
|
||||
items={menu}
|
||||
/>
|
||||
</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"}
|
||||
<Menu vertical
|
||||
items={menu}
|
||||
/>
|
||||
</nav>
|
||||
)}
|
||||
</Box>
|
||||
</Overlay>
|
||||
|
||||
<Box wrap style={{
|
||||
padding: "var(--size-s)",
|
||||
}}>
|
||||
{collapseMenu ? null : (
|
||||
<Menu
|
||||
items={menu}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Spacer />
|
||||
|
||||
{!collapseMenu ? null : (
|
||||
<MenuToggle
|
||||
open={open}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import s from "./styles.module.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 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}
|
||||
>
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={s.overlay}
|
||||
style={props.style}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { PageMeta } from "../PageMeta"
|
||||
import { ITag } from "src/types"
|
||||
|
||||
export type PageProps = {
|
||||
children?: React.ReactNode
|
||||
style?: React.CSSProperties
|
||||
date?: Date
|
||||
tags: ITag[]
|
||||
|
||||
@@ -3,8 +3,8 @@ import { PageCardDto } from "@/types"
|
||||
import { CardGrid } from "../CardGrid"
|
||||
import { Card, CardLayout } from "../Card"
|
||||
import { Date } from "./Date"
|
||||
import { Block } from "../Block"
|
||||
import { Spacer } from "../Spacer"
|
||||
import { Box } from "@/ui/Box"
|
||||
|
||||
export type PageGridProps = {
|
||||
items: PageCardDto[]
|
||||
@@ -23,9 +23,7 @@ export const PageGrid: React.FC<PageGridProps> = props => {
|
||||
|
||||
const content = !item.featured
|
||||
? (
|
||||
<Block
|
||||
direction={"vertical"}
|
||||
>
|
||||
<Box vertical>
|
||||
{item.title}
|
||||
<Spacer />
|
||||
{!item.date ? null : (
|
||||
@@ -33,7 +31,7 @@ export const PageGrid: React.FC<PageGridProps> = props => {
|
||||
marginTop: "var(--size-m)",
|
||||
}}>{item.date}</Date>
|
||||
)}
|
||||
</Block>
|
||||
</Box>
|
||||
) : (
|
||||
item.title
|
||||
)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Block, BlockProps } from "../Block"
|
||||
import { Button } from "../Button"
|
||||
import { Spacer } from "../Spacer"
|
||||
|
||||
export type PagerProps = Omit<BlockProps, "direction"> & {
|
||||
nextHref: string | null
|
||||
prevHref: string | null
|
||||
}
|
||||
|
||||
export const Pager: React.FC<PagerProps> = ({ prevHref, nextHref, ...props }) => (
|
||||
<Block {...props} direction={"horizontal"}>
|
||||
{!prevHref ? null : (
|
||||
<Button href={prevHref}>Предыдущая страница</Button>
|
||||
)}
|
||||
<Spacer />
|
||||
{!nextHref ? null : (
|
||||
<Button href={nextHref}>Следущая страница</Button>
|
||||
)}
|
||||
</Block>
|
||||
)
|
||||
@@ -43,7 +43,7 @@ export const Picture: React.FC<PictureProps> = memo(({ wide = false, ...props })
|
||||
height={props.height}
|
||||
layout={"responsive"}
|
||||
blurDataURL={props.blur}
|
||||
placeholder={"blur"}
|
||||
placeholder={props.blur ? "blur": "empty"}
|
||||
/>
|
||||
{!props.caption ? null : (
|
||||
<figcaption>{props.caption}</figcaption>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import s from './styles.module.css'
|
||||
import s from "./styles.module.css"
|
||||
|
||||
import cx from 'classnames'
|
||||
import { ITag } from 'src/types'
|
||||
import { Button } from '../Button'
|
||||
import cx from "classnames"
|
||||
import { ITag } from "src/types"
|
||||
import { Button } from "@/ui/Button"
|
||||
import Image from "next/image"
|
||||
|
||||
const directionClass = {
|
||||
horizontal: '',
|
||||
horizontal: "",
|
||||
vertical: s.vertical,
|
||||
}
|
||||
|
||||
export type Direction = 'horizontal' | 'vertical'
|
||||
export type Direction = "horizontal" | "vertical"
|
||||
|
||||
export type TagProps = {
|
||||
children?: React.ReactNode
|
||||
href: string
|
||||
direction: Direction
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export type WrapperProps = {
|
||||
footer: React.ReactNode
|
||||
style?: React.CSSProperties
|
||||
mainStyle?: React.CSSProperties
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Wrapper: React.FC<WrapperProps> = props => (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-back-main);
|
||||
|
||||
font-family: var(--text-font);
|
||||
@@ -9,5 +8,6 @@
|
||||
}
|
||||
|
||||
.main {
|
||||
min-height: 100vh;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -40,34 +40,6 @@ const config = {
|
||||
name: "Школа",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
name: "Поступление",
|
||||
href: "/join",
|
||||
},
|
||||
{
|
||||
name: "Предметы",
|
||||
href: "/courses",
|
||||
},
|
||||
{
|
||||
name: "Коллектив",
|
||||
href: "/collective",
|
||||
},
|
||||
{
|
||||
name: "«Мы соседи»",
|
||||
href: "/neighbors",
|
||||
},
|
||||
{
|
||||
name: "Документы",
|
||||
href: "/documents",
|
||||
},
|
||||
{
|
||||
name: "Сведения об образовательной организации",
|
||||
href: "/info",
|
||||
},
|
||||
{
|
||||
name: "Дистанционное образование",
|
||||
href: "/info/online-education",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
export function isPartOfPath(part: string, path: string): boolean {
|
||||
return path.startsWith(part)
|
||||
}
|
||||
|
||||
export function queryList(key: string, values: string[]): string {
|
||||
if (values.length === 0) {
|
||||
return ""
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.article a {
|
||||
color: var(--color-text);
|
||||
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--color-link);
|
||||
}
|
||||
|
||||
.article a:hover {
|
||||
border-bottom: 2px solid var(--color-text);
|
||||
}
|
||||
|
||||
.article a:active {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.article h1 {
|
||||
margin-bottom: var(--size-l);
|
||||
}
|
||||
|
||||
+7
-23
@@ -44,14 +44,16 @@
|
||||
|
||||
/* EFFECTS */
|
||||
--radius: 16px;
|
||||
--shadow: 0 0 8px var(--color-shadow);
|
||||
--shadow:
|
||||
0 2px 4px rgba(27, 51, 88, 0.2),
|
||||
0 4px 8px -8px rgba(0, 0, 0, 0.2),
|
||||
0 8px 16px -8px rgba(0, 0, 0, 0.3);
|
||||
--shadow-active:
|
||||
0 12px 24px -8px rgba(27, 51, 88, 0.25),
|
||||
0 8px 16px -8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* SPECIAL */
|
||||
--article-line-height: 1.5em;
|
||||
--menu-mark-thickness: 4px;
|
||||
--embed-video-ratio: 56.25%;
|
||||
|
||||
/* CONTENT */
|
||||
@@ -108,24 +110,9 @@ h3 {
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-text);
|
||||
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--color-link);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
border-bottom: 2px solid var(--color-text);
|
||||
}
|
||||
|
||||
a:active {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
a.invisible {
|
||||
/* a.invisible {
|
||||
border-bottom: none;
|
||||
}
|
||||
} */
|
||||
|
||||
.opposite a {
|
||||
color: var(--color-text-opposite);
|
||||
@@ -136,10 +123,6 @@ a.invisible {
|
||||
border-bottom-color: var(--color-text-opposite);
|
||||
}
|
||||
|
||||
.noscroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* RC TOOLTIP OVERRIDE */
|
||||
.hudozka.rc-tooltip {
|
||||
@@ -199,6 +182,7 @@ a.invisible {
|
||||
|
||||
--color-back-main: hsl(240, 10%, 10%);
|
||||
--color-back: hsl(240, 10%, 15%);
|
||||
--color-back-active: hsl(240, 10%, 20%);
|
||||
--color-back-opposite: hsl(240, 10%, 80%);
|
||||
--color-link: hsl(240, 10%, 40%);
|
||||
--color-link-opposite: hsl(240, 10%, 60%);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
.box {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
justify-content: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.box.align {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.box.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.box.center {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.box.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import s from "./box.module.css"
|
||||
|
||||
import classnames from "classnames/bind"
|
||||
import { createElement } from "react"
|
||||
|
||||
const cx = classnames.bind(s)
|
||||
|
||||
export type BoxProps = {
|
||||
as?: keyof HTMLElementTagNameMap
|
||||
children?: React.ReactNode
|
||||
style?: React.CSSProperties
|
||||
wrap?: boolean
|
||||
align?: boolean
|
||||
vertical?: boolean
|
||||
center?: boolean
|
||||
}
|
||||
|
||||
export const Box: React.FC<BoxProps> = ({
|
||||
as = "div",
|
||||
wrap = false,
|
||||
vertical = false,
|
||||
center = false,
|
||||
align = true,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
const newProps = {
|
||||
className: cx(s.box, { vertical, align, center, wrap }),
|
||||
style,
|
||||
}
|
||||
|
||||
return createElement(as, newProps, props.children)
|
||||
}
|
||||
@@ -1,28 +1,26 @@
|
||||
.button {
|
||||
display: block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
font-size: var(--font-size-default);
|
||||
|
||||
text-align: center;
|
||||
|
||||
border-radius: 100px;
|
||||
border-radius: var(--size-s);
|
||||
border: 1px solid var(--color-shadow);
|
||||
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-back);
|
||||
|
||||
transition: box-shadow 150ms;
|
||||
box-shadow: none;
|
||||
transition: box-shadow 200ms ease-in, background-color 200ms ease-in;
|
||||
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
margin: 1px;
|
||||
/* margin-left: 1px;
|
||||
margin-right: 1px; */
|
||||
|
||||
border: none;
|
||||
box-shadow: var(--shadow-active);
|
||||
border-color: rgba(0, 0, 0, 0);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* MODIFICATORS */
|
||||
@@ -50,5 +48,13 @@
|
||||
}
|
||||
|
||||
.themeIcon {
|
||||
border-color: rgba(0, 0, 0, 0);
|
||||
padding: var(--size-xs) var(--size-xs);
|
||||
}
|
||||
|
||||
/* DARK THEME */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.button:hover {
|
||||
background-color: var(--color-back-active);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import s from "./styles.module.css"
|
||||
import s from "./button.module.css"
|
||||
|
||||
import Link from "next/link"
|
||||
import cx from "classnames"
|
||||
import { useCallback } from "react"
|
||||
import type { MouseEvent, MouseEventHandler } from "react"
|
||||
|
||||
const sizeClass = {
|
||||
default: s.sizeDefault,
|
||||
@@ -16,6 +17,7 @@ const themeClass = {
|
||||
}
|
||||
|
||||
export type ButtonProps = {
|
||||
children?: React.ReactNode
|
||||
value?: any
|
||||
style?: React.CSSProperties
|
||||
href?: string
|
||||
@@ -29,11 +31,11 @@ export const Button: React.FC<ButtonProps> = ({ size = "default", theme = "defau
|
||||
const className = cx(s.button, sizeClass[size], themeClass[theme], {
|
||||
[s.disabled]: disabled,
|
||||
})
|
||||
const onClick = useCallback(event => {
|
||||
if (props.onClick) {
|
||||
const onClick = useCallback<MouseEventHandler<HTMLButtonElement>>(event => {
|
||||
if (typeof props.onClick === "function") {
|
||||
props.onClick(props.value, event)
|
||||
}
|
||||
}, [props.value, props.onClick])
|
||||
}, [props])
|
||||
|
||||
if (props.href) {
|
||||
return (
|
||||
@@ -1,25 +1,26 @@
|
||||
import * as React from "react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
|
||||
export interface ITransitionProps {
|
||||
export type TransitionProps = {
|
||||
children?: React.ReactNode
|
||||
duration: number
|
||||
extraStyle?: React.CSSProperties
|
||||
show: boolean
|
||||
}
|
||||
|
||||
export const Transition: React.FC<ITransitionProps> = props => {
|
||||
const [mounted, setMounted] = React.useState(props.show)
|
||||
const [opacity, setOpacity] = React.useState(0)
|
||||
const onTransitionEnd = React.useCallback(() => {
|
||||
export const Transition: React.FC<TransitionProps> = props => {
|
||||
const [mounted, setMounted] = useState(props.show)
|
||||
const [opacity, setOpacity] = useState(0)
|
||||
const onTransitionEnd = useCallback(() => {
|
||||
if (opacity === 0) {
|
||||
setMounted(false)
|
||||
}
|
||||
}, [opacity])
|
||||
const transition = React.useMemo(
|
||||
const transition = useMemo(
|
||||
() => `all ${props.duration}ms`,
|
||||
[props.duration]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!props.show) {
|
||||
setOpacity(0)
|
||||
return
|
||||
@@ -0,0 +1,39 @@
|
||||
import s from "./styles.module.css"
|
||||
|
||||
import { Transition } from "./Transition"
|
||||
import { useReducedMotion } from "../../hooks/useReducedMotion"
|
||||
import { useLockBodyScroll } from "react-use"
|
||||
|
||||
export type OverlayProps = {
|
||||
children?: React.ReactNode
|
||||
show: boolean
|
||||
onClickOverlay: () => void
|
||||
style?: React.CSSProperties
|
||||
duration: number
|
||||
}
|
||||
|
||||
export const Overlay: React.FC<OverlayProps> = props => {
|
||||
const motionDisabled = useReducedMotion()
|
||||
const duration = motionDisabled ? 0 : props.duration
|
||||
|
||||
useLockBodyScroll(props.show)
|
||||
|
||||
return (
|
||||
<Transition
|
||||
duration={duration}
|
||||
show={props.show}
|
||||
>
|
||||
<div
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
props.onClickOverlay()
|
||||
}
|
||||
}}
|
||||
className={s.overlay}
|
||||
style={props.style}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
Reference in new issue
Block a user