17 files changed
+303
-191
No files matched your search
+1
-1
@@ -4,7 +4,7 @@ const withSass = require('@zeit/next-sass')
|
||||
module.exports = withCss(withSass({
|
||||
exportTrailingSlash: true,
|
||||
env: {
|
||||
APP_ARTICLES_PAGE_SIZE: 15,
|
||||
APP_ARTICLES_PAGE_SIZE: 12,
|
||||
},
|
||||
webpack(config) {
|
||||
config.resolve.modules.push(__dirname)
|
||||
|
||||
@@ -38,14 +38,14 @@ export const App: React.FC<IAppProps> = props => {
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<section className={cx('content', `content_${props.layout}`, {
|
||||
<div className={cx('content', `content_${props.layout}`, {
|
||||
'content--padding-top--menu': props.menuPadding,
|
||||
'reduced-motion': motionDisabled,
|
||||
})}>
|
||||
{props.children}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Comments />
|
||||
{/* <Comments /> */}
|
||||
</Wrapper>
|
||||
</ConfigContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,123 +1,43 @@
|
||||
import * as React from 'react'
|
||||
import './styles.css'
|
||||
|
||||
import cx from 'classnames'
|
||||
import { Date } from './Date'
|
||||
|
||||
interface IImage {
|
||||
alt: string
|
||||
src: string
|
||||
srcSet: Array<{ url: string, density: number }>
|
||||
}
|
||||
import { IImage } from 'src/types'
|
||||
import Link from 'next/link'
|
||||
|
||||
export interface IArticleCardProps {
|
||||
style?: React.CSSProperties
|
||||
article: any
|
||||
preview?: IImage
|
||||
}
|
||||
|
||||
export const ArticleCard: React.FC<IArticleCardProps> = ({ article, ...props }) => (
|
||||
<section className="ArticleCard">
|
||||
<style jsx>{`
|
||||
.ArticleCard {
|
||||
--article-card-width: 30%;
|
||||
--article-card-image-height: 15em;
|
||||
--article-card-margin: var(--double-margin) var(--single-margin);
|
||||
<div className="ArticleCard" style={props.style}>
|
||||
<Link href={article.url}>
|
||||
<a className="invisible">
|
||||
<div className="ArticleCard-image">
|
||||
{!props.preview ? null : (
|
||||
<picture>
|
||||
<img
|
||||
className={cx({ opa: false })}
|
||||
alt={props.preview.alt}
|
||||
src={props.preview.src}
|
||||
srcSet={props.preview.srcSet.map(({ url, density }) => `${url} ${density}x`).join(' ')}
|
||||
/>
|
||||
</picture >
|
||||
)}
|
||||
</div>
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
<div className="ArticleCard-body">
|
||||
{article.title}
|
||||
|
||||
margin: var(--article-card-margin);
|
||||
width: var(--article-card-width);
|
||||
min-width: 15em;
|
||||
max-width: 20em;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ArticleCard > a {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ArticleCard-image {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: var(--article-card-image-height);
|
||||
}
|
||||
|
||||
.ArticleCard-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
|
||||
transition: all 150ms ease-out;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ArticleCard-body {
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
height: 5em;
|
||||
|
||||
padding: .75em 1em;
|
||||
}
|
||||
|
||||
.ArticleCard:hover img {
|
||||
width: 105%;
|
||||
height: 105%;
|
||||
|
||||
top: -2.5%;
|
||||
left: -2.5%;
|
||||
}
|
||||
|
||||
// Tablet
|
||||
@media screen and (min-width: var(--tablet-min-width)) and (max-width: var(--tablet-max-width)) {
|
||||
.ArticleCard {
|
||||
--article-card-image-height: 12em;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile
|
||||
@media (max-width: 31.25em) {
|
||||
.ArticleCard {
|
||||
--article-card-width: 100%;
|
||||
--article-card-margin: var(--single-margin) 0;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<a className="invisible" href={article.url}>
|
||||
<div className="ArticleCard-image">
|
||||
{!props.preview ? null : (
|
||||
<picture>
|
||||
<img
|
||||
className={cx({ opa: false })}
|
||||
alt={props.preview.alt}
|
||||
src={props.preview.src}
|
||||
srcSet={props.preview.srcSet.map(({ url, density }) => `${url} ${density}x`).join(' ')}
|
||||
/>
|
||||
</picture >
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ArticleCard-body">
|
||||
{article.title}
|
||||
|
||||
{!article.date ? null : (
|
||||
<Date>
|
||||
{article.date}
|
||||
</Date>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</section>
|
||||
{!article.date ? null : (
|
||||
<Date>
|
||||
{article.date}
|
||||
</Date>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
.ArticleCard {
|
||||
--article-card-image-height: 15em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
/* box-shadow: 0 0 0 1px var(--color-shadow); */
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
transition: box-shadow 150ms;
|
||||
}
|
||||
|
||||
.ArticleCard:hover {
|
||||
box-shadow: var(--shadow-active);
|
||||
}
|
||||
|
||||
.ArticleCard > a {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ArticleCard-image {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: var(--article-card-image-height);
|
||||
}
|
||||
|
||||
.ArticleCard-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
|
||||
transition: all 150ms ease-out;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ArticleCard-body {
|
||||
position: relative;
|
||||
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
height: 5em;
|
||||
|
||||
background-color: white;
|
||||
|
||||
padding: 0 var(--size-m) var(--size-m);
|
||||
}
|
||||
|
||||
.ArticleCard-body:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background-color: white;
|
||||
|
||||
border-radius: var(--radius) var(--radius) 0 0 ;
|
||||
width: 100%;
|
||||
height: var(--radius);
|
||||
top: calc(var(--radius) * -1);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ArticleCard:hover img {
|
||||
width: 104%;
|
||||
height: 104%;
|
||||
|
||||
top: -2%;
|
||||
left: -2%;
|
||||
}
|
||||
|
||||
/* Tablet */
|
||||
@media screen and (min-width: var(--tablet-min-width)) and (max-width: var(--tablet-max-width)) {
|
||||
.ArticleCard {
|
||||
--article-card-image-height: 12em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 31.25em) {
|
||||
.ArticleCard {
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,21 @@
|
||||
import Paginator from './Paginator'
|
||||
import { ArticleCard } from './ArticleCard'
|
||||
import { Pager } from './Pager'
|
||||
import { CardGrid } from './CardGrid'
|
||||
import { IArticle } from 'src/types'
|
||||
|
||||
export interface IArticleCardListProps {
|
||||
articles: any[]
|
||||
articles: IArticle[]
|
||||
nextPage: number | null
|
||||
prevPage: number | null
|
||||
}
|
||||
|
||||
export const ArticleCardList:React.FC<IArticleCardListProps> = ({ articles, nextPage, prevPage }) => (
|
||||
<div>
|
||||
<style jsx>{`
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{!prevPage ? null : (
|
||||
<Paginator url={`/articles/${prevPage}`} type="top" />
|
||||
)}
|
||||
|
||||
<div className={'body'}>
|
||||
export const ArticleCardList: React.FC<IArticleCardListProps> = ({ articles, nextPage, prevPage, ...props }) => (
|
||||
<>
|
||||
<CardGrid
|
||||
style={{
|
||||
marginBottom: 'var(--size-m)',
|
||||
}}
|
||||
>
|
||||
{articles.map((article, i) => {
|
||||
const preview = {
|
||||
alt: article.title,
|
||||
@@ -30,18 +23,24 @@ export const ArticleCardList:React.FC<IArticleCardListProps> = ({ articles, next
|
||||
srcSet: article.preview.set,
|
||||
}
|
||||
|
||||
const gridColumn = article.featured ? 'span 2' : 'auto'
|
||||
|
||||
return (
|
||||
<ArticleCard
|
||||
key={i}
|
||||
article={article}
|
||||
preview={preview}
|
||||
preview={preview as any}
|
||||
style={{
|
||||
gridColumn
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardGrid>
|
||||
|
||||
{!nextPage ? null : (
|
||||
<Paginator url={`/articles/${nextPage}`} type="bottom" />
|
||||
)}
|
||||
</div>
|
||||
<Pager
|
||||
nextHref={nextPage ? `/articles/${nextPage}` : null}
|
||||
prevHref={prevPage ? `/articles/${prevPage}` : null}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
import './styles.css'
|
||||
import Link from 'next/link'
|
||||
|
||||
export type ButtonProps = {
|
||||
style?: React.CSSProperties
|
||||
href?: string
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = props => {
|
||||
if (props.href) {
|
||||
return (
|
||||
<Link href={props.href}>
|
||||
<a className={'button invisible'} style={props.style}>
|
||||
{props.children}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={'button'} style={props.style}>
|
||||
{props.children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
.button {
|
||||
border: none;
|
||||
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;
|
||||
|
||||
background-color: var(--color-back);
|
||||
box-shadow: 0 0 0 1px var(--color-shadow);
|
||||
|
||||
transition: box-shadow 150ms;
|
||||
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
box-shadow: var(--shadow-active);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import './styles.css'
|
||||
|
||||
export type CardGridProps = {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export const CardGrid: React.FC<CardGridProps> = props => {
|
||||
return (
|
||||
<section className={'card-grid'} style={props.style}>
|
||||
{props.children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.card-grid {
|
||||
--min-cell-size: 240px;
|
||||
display: grid;
|
||||
/* grid-template-columns: 1fr 1fr 1fr; */
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--min-cell-size), 1fr));
|
||||
grid-auto-rows: 1fr;
|
||||
grid-gap: var(--size-xl) var(--size-l);
|
||||
}
|
||||
|
||||
/* .card-grid > *:first-child {
|
||||
grid-row: 1 / 1;
|
||||
grid-column: 1 / 1;
|
||||
} */
|
||||
|
||||
@media screen and (max-width: 31.25em) {
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
/* grid-template-columns: auto; */
|
||||
grid-gap: var(--size-l) 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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>
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
const React = require('react')
|
||||
|
||||
const text = ['↑', '↓']
|
||||
const linkText = ['Предыдущая страница', 'Следующая страница']
|
||||
|
||||
const i = type => type === 'top'
|
||||
? 0
|
||||
: 1
|
||||
|
||||
const Icon = ({children}) => (
|
||||
<span className="Paginator-Icon">{children}</span>
|
||||
)
|
||||
|
||||
const Paginator = ({url, type}) => (
|
||||
<div className="Paginator">
|
||||
<Icon>{text[i(type)]}</Icon>
|
||||
<a href={url}>{linkText[i(type)]}</a>
|
||||
</div>
|
||||
)
|
||||
|
||||
module.exports = Paginator
|
||||
@@ -21,11 +21,16 @@ export const Wrapper: React.FC<WrapperProps> = props => (
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{props.header}
|
||||
|
||||
<main className={'content'}>
|
||||
<main>
|
||||
{props.children}
|
||||
</main>
|
||||
|
||||
|
||||
+11
-3
@@ -23,10 +23,12 @@
|
||||
--tablet-min-width: 31.25em;
|
||||
--tablet-max-width: 64em;
|
||||
|
||||
/* CONTENT */
|
||||
--content-full: 100%;
|
||||
--content-wide: 90%;
|
||||
--content-semi-wide: 70%;
|
||||
--content-thin: 60%;
|
||||
--content-width: var(--content-full);
|
||||
|
||||
--embed-video-ratio: 56.25%;
|
||||
|
||||
@@ -53,6 +55,7 @@
|
||||
--magenta-color: rgb(214, 39, 246);
|
||||
--red-color: rgb(246, 27, 20);
|
||||
--gray-color: rgb(153, 154, 160);
|
||||
--color-shadow: rgba(27, 51, 88, 0.15);
|
||||
|
||||
--inactive-link-color: var(--light-color);
|
||||
|
||||
@@ -71,7 +74,12 @@
|
||||
--size-s: 0.5em;
|
||||
--size-m: 1em;
|
||||
--size-l: 2em;
|
||||
--size-xl: 5em;
|
||||
--size-xl: 3em;
|
||||
|
||||
/* EFFECTS */
|
||||
--radius: 15px;
|
||||
--shadow: 0 0 8px var(--color-shadow);
|
||||
--shadow-active: 0 4px 16px var(--color-shadow);
|
||||
|
||||
/* MARGINS */
|
||||
--small-margin: 0.2em;
|
||||
@@ -149,9 +157,9 @@ a:active {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
/* a:visited {
|
||||
color: #a4059c;
|
||||
}
|
||||
} */
|
||||
|
||||
a.invisible {
|
||||
border-bottom: none;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
.Paginator {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-second);
|
||||
|
||||
margin: var(--single-margin) 0;
|
||||
}
|
||||
|
||||
.Paginator-Icon {
|
||||
margin: 0 10px;
|
||||
}
|
||||
+18
-16
@@ -1,25 +1,27 @@
|
||||
.content {
|
||||
margin: 0 auto 1em;
|
||||
// margin: 0 auto 1em;
|
||||
|
||||
&.content_full {
|
||||
width: var(--content-full);
|
||||
}
|
||||
width: var(--content-width);
|
||||
}
|
||||
|
||||
&.content_wide {
|
||||
width: var(--content-wide);
|
||||
}
|
||||
.content_full {
|
||||
--content-width: var(--content-full);
|
||||
}
|
||||
|
||||
&.content_semi-wide {
|
||||
width: var(--content-semi-wide);
|
||||
}
|
||||
.content_wide {
|
||||
--content-width: var(--content-wide);
|
||||
}
|
||||
|
||||
&.content_thin {
|
||||
width: var(--content-thin);
|
||||
}
|
||||
.content_semi-wide {
|
||||
--content-width: var(--content-semi-wide);
|
||||
}
|
||||
|
||||
&.content--padding-top--menu {
|
||||
padding-top: var(--menu-thickness);
|
||||
}
|
||||
.content_thin {
|
||||
--content-width: var(--content-thin);
|
||||
}
|
||||
|
||||
.content--padding-top--menu {
|
||||
padding-top: var(--menu-thickness);
|
||||
}
|
||||
|
||||
// Animation on load
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
@import "components/schedule-table";
|
||||
@import "components/footer-links";
|
||||
@import "components/Article";
|
||||
@import "components/Paginator";
|
||||
@import "components/album";
|
||||
@import "components/ArticleTags";
|
||||
|
||||
|
||||
@@ -23,3 +23,18 @@ export interface IMeta {
|
||||
twitterSite: string
|
||||
twitterCreator: string
|
||||
}
|
||||
|
||||
export interface IImage {
|
||||
alt: string
|
||||
src: string
|
||||
srcSet: Array<{ url: string, density: number }>
|
||||
set: any // TODO
|
||||
}
|
||||
|
||||
export interface IArticle {
|
||||
id: string
|
||||
featured: boolean
|
||||
url: string
|
||||
title: string
|
||||
preview: IImage
|
||||
}
|
||||
Reference in new issue
Block a user