Merge pull request #67 from tmshv/upd/strapi-integration
Strapi integration
This commit is contained in:
29 files changed
+1588
-4957
No files matched your search
+2
-1
@@ -19,4 +19,5 @@ dist
|
||||
/coverage
|
||||
**/.vscode
|
||||
*.log
|
||||
.now
|
||||
.now
|
||||
.vercel
|
||||
+7
-1
@@ -1,7 +1,13 @@
|
||||
module.exports = {
|
||||
trailingSlash: true,
|
||||
trailingSlash: false,
|
||||
env: {
|
||||
APP_ARTICLES_PAGE_SIZE: 24,
|
||||
APP_CARD_DEFAULT_IMAGE: 'https://art.shlisselburg.org/static/img/main.jpg',
|
||||
},
|
||||
images: {
|
||||
domains: [
|
||||
'hudozkacdn.tmshv.com',
|
||||
'images.weserv.nl',
|
||||
],
|
||||
},
|
||||
}
|
||||
Generated
+1020
-4451
File diff suppressed because it is too large.
Load diff
+6
-6
@@ -4,10 +4,10 @@
|
||||
"version": "6.13.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "next dev",
|
||||
"build": "npm run next-build && npm run export && npm run sitemap && npm run feed",
|
||||
"start": "next start",
|
||||
"dev": "next",
|
||||
"build": "npm run next-build && npm run sitemap && npm run feed",
|
||||
"next-build": "next build",
|
||||
"export": "next export -o dist",
|
||||
"sitemap": "node scripts/sitemap.js",
|
||||
"feed": "node scripts/feed.js"
|
||||
},
|
||||
@@ -23,9 +23,9 @@
|
||||
"date-fns": "^2.11.1",
|
||||
"lodash": "^4.17.19",
|
||||
"markdown-it": "^10.0.0",
|
||||
"next": "^9.5.4",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"next": "^10.0.6",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"sitemap": "^1.5.0",
|
||||
"typograf": "^6.11.1",
|
||||
"use-media": "^1.4.0"
|
||||
|
||||
+65
-53
@@ -1,41 +1,38 @@
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import { App } from 'src/components/App'
|
||||
import { tail } from 'lodash'
|
||||
import { Page } from 'src/components/Page'
|
||||
import { Markdown } from 'src/components/Markdown'
|
||||
import { Meta } from 'src/components/Meta'
|
||||
import { MetaBuilder } from 'src/lib/meta'
|
||||
import { createApiUrl, requestGet, IResponseItems } from 'src/next-lib'
|
||||
import { NextPage } from 'next'
|
||||
import { IBreadcumbsPart, IMeta, IPage, ITag, FileTokenData, Token } from 'src/types'
|
||||
import { joinTokens } from 'src/lib/tokens'
|
||||
import { apiGet } from '@/next-lib'
|
||||
import { GetStaticProps, NextPage } from 'next'
|
||||
import { IBreadcumbsPart, IMeta, ITag, FileTokenData, Token } from '@/types'
|
||||
import { size, ext } from 'src/lib/file'
|
||||
import { Html } from 'src/components/Html'
|
||||
import { Youtube } from '@/components/Youtube'
|
||||
import { createPage, createPageUrls } from '@/remote/factory'
|
||||
import { paramsToSlug } from '@/remote/lib'
|
||||
import { PageGrid } from '@/components/PageGrid'
|
||||
|
||||
function array<T>(value: T | T[]) {
|
||||
return Array.isArray(value)
|
||||
? value
|
||||
: [value]
|
||||
}
|
||||
async function getUrls() {
|
||||
let urls = []
|
||||
|
||||
const Youtube: React.SFC<{ url: string }> = props => {
|
||||
const url = new URL(props.url)
|
||||
const videoId = url.searchParams.get('v')
|
||||
if (!videoId) {
|
||||
return null
|
||||
const limit = 100
|
||||
let start = 0
|
||||
while (true) {
|
||||
const url = `https://hudozka.tmshv.com/pages?_limit=${limit}&_start=${start}`
|
||||
const res = await apiGet(createPageUrls)(url, null)
|
||||
if (!res || res.items.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
start += limit
|
||||
urls = [...urls, ...res.items]
|
||||
}
|
||||
|
||||
const src = `//www.youtube.com/embed/${videoId}`
|
||||
|
||||
return (
|
||||
<div className="kazimir__video">
|
||||
<iframe
|
||||
src={src}
|
||||
frameBorder="0"
|
||||
// allowFullscreen
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
return urls
|
||||
}
|
||||
|
||||
const File: React.SFC<FileTokenData> = props => {
|
||||
@@ -46,7 +43,11 @@ const File: React.SFC<FileTokenData> = props => {
|
||||
<div className={'document-row'}>
|
||||
<a href={props['url']} className="invisible">
|
||||
<div className="document-row__image">
|
||||
<img src={props['image_url']} alt={props['title']} />
|
||||
<Image
|
||||
src={props['image_url']}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -69,7 +70,7 @@ type Props = {
|
||||
date: string
|
||||
breadcrumb: IBreadcumbsPart[]
|
||||
meta?: IMeta
|
||||
tokens?: Token[]
|
||||
tokens: Token[]
|
||||
}
|
||||
|
||||
const Index: NextPage<Props> = props => (
|
||||
@@ -92,11 +93,12 @@ const Index: NextPage<Props> = props => (
|
||||
date={props.date ? new Date(props.date) : null}
|
||||
>
|
||||
<article className={'article'}>
|
||||
{joinTokens(props.tokens ?? []).map((x, i) => {
|
||||
{props.tokens.map((x, i) => {
|
||||
switch (x.token) {
|
||||
case 'text':
|
||||
return (
|
||||
<Markdown
|
||||
key={i}
|
||||
data={x.data}
|
||||
/>
|
||||
)
|
||||
@@ -104,6 +106,7 @@ const Index: NextPage<Props> = props => (
|
||||
case 'html':
|
||||
return (
|
||||
<Html
|
||||
key={i}
|
||||
html={x.data}
|
||||
/>
|
||||
)
|
||||
@@ -111,6 +114,7 @@ const Index: NextPage<Props> = props => (
|
||||
case 'instagram':
|
||||
return (
|
||||
<Html
|
||||
key={i}
|
||||
html={x.data.embed}
|
||||
/>
|
||||
)
|
||||
@@ -118,31 +122,41 @@ const Index: NextPage<Props> = props => (
|
||||
case 'youtube':
|
||||
return (
|
||||
<Youtube
|
||||
key={i}
|
||||
url={x.data.url}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'image':
|
||||
return (
|
||||
<div className="kazimir__image">
|
||||
<figure>
|
||||
<img
|
||||
src={x.data.src}
|
||||
alt={x.data.alt}
|
||||
/>
|
||||
<figcaption>{x.data.caption}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<figure key={i} className="kazimir__image">
|
||||
<Image
|
||||
src={x.data.src}
|
||||
alt={x.data.alt}
|
||||
width={x.data.width}
|
||||
height={x.data.height}
|
||||
layout={'responsive'}
|
||||
/>
|
||||
<figcaption>{x.data.caption}</figcaption>
|
||||
</figure>
|
||||
)
|
||||
|
||||
case 'file':
|
||||
return (
|
||||
<File {...x.data} />
|
||||
<File key={i} {...x.data} />
|
||||
)
|
||||
|
||||
case 'grid':
|
||||
return (
|
||||
<PageGrid
|
||||
key={i}
|
||||
items={x.data.items}
|
||||
/>
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<pre>
|
||||
<pre key={i}>
|
||||
{JSON.stringify(x)}
|
||||
</pre>
|
||||
)
|
||||
@@ -153,17 +167,18 @@ const Index: NextPage<Props> = props => (
|
||||
</App>
|
||||
)
|
||||
|
||||
export const getStaticProps = async (ctx: any) => {
|
||||
let slug = null
|
||||
if (ctx.query) {
|
||||
slug = '/' + array(ctx.query.slug).join('/')
|
||||
} else {
|
||||
slug = '/' + array(ctx.params.slug).join('/')
|
||||
}
|
||||
const page = await requestGet<IPage | null>(createApiUrl(`/api/page?page=${slug}`), null)
|
||||
export const getStaticProps: GetStaticProps<any> = async ctx => {
|
||||
const slug = paramsToSlug(ctx.params.slug)
|
||||
const url = `https://hudozka.tmshv.com/pages?slug=${slug}`
|
||||
const page = await apiGet(createPage)(url, null)
|
||||
if (!page) {
|
||||
throw new Error(`Not found: ${slug}`)
|
||||
console.log('kek something happend');
|
||||
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
}
|
||||
|
||||
const description = page.description ?? undefined
|
||||
const breadcrumbSize = page?.breadcrumb?.length ?? 0
|
||||
const breadcrumb = breadcrumbSize < 2 ? null : page.breadcrumb
|
||||
@@ -187,14 +202,11 @@ export const getStaticProps = async (ctx: any) => {
|
||||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const urls = await requestGet<IResponseItems<string> | null>(createApiUrl(`/api/pages/urls`), null)
|
||||
if (!urls) {
|
||||
return null
|
||||
}
|
||||
const urls = await getUrls()
|
||||
|
||||
return {
|
||||
fallback: false,
|
||||
paths: urls.items
|
||||
paths: urls
|
||||
.map(path => {
|
||||
const slug = tail(path.split('/'))
|
||||
|
||||
|
||||
@@ -52,13 +52,6 @@ export default class MyApp extends App {
|
||||
as="style"
|
||||
/>
|
||||
|
||||
{/* Likely */}
|
||||
<script src='/static/likely.js' />
|
||||
<link href='/static/likely.css' rel='stylesheet' />
|
||||
|
||||
{/* Instagram Embed */}
|
||||
<script async defer src="//platform.instagram.com/en_US/embeds.js" />
|
||||
|
||||
<YMetrika account={24027460} />
|
||||
<GAnalytics account={'UA-60209057-1'} />
|
||||
</Head>
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import Head from 'next/head'
|
||||
import { App } from 'src/components/App'
|
||||
import { Meta } from 'src/components/Meta'
|
||||
import { meta } from 'src/lib/meta'
|
||||
import { IMeta, IPage, ImageSize } from 'src/types'
|
||||
import { createApiUrl, requestGet } from 'src/next-lib'
|
||||
import { NextPage, NextPageContext } from 'next'
|
||||
import { CardGrid } from 'src/components/CardGrid'
|
||||
import { Card } from 'src/components/Card'
|
||||
import { Block } from 'src/components/Block'
|
||||
import { Image } from 'src/components/Image'
|
||||
import { imageSrcSet } from 'src/lib/image'
|
||||
|
||||
const persons = [
|
||||
'/teacher/mg-timasheva',
|
||||
'/teacher/va-sarzhin',
|
||||
'/teacher/od-gogoleva',
|
||||
'/teacher/nv-andreeva',
|
||||
'/teacher/vv-voronova',
|
||||
'/teacher/ea-burovtseva',
|
||||
'/teacher/sa-latipova',
|
||||
//'/teacher/iv-khramov',
|
||||
//'/teacher/ma-khramova',
|
||||
'/teacher/av-belyaeva',
|
||||
'/teacher/aa-khopryaninova',
|
||||
'/teacher/ay-orlova',
|
||||
'/teacher/ms-strukova',
|
||||
// АС Тимашева
|
||||
// ЕЮ Тарасова
|
||||
// ИН Втюрина
|
||||
// МЮ Валькова
|
||||
// РК Тимашев
|
||||
]
|
||||
|
||||
type CardData = {
|
||||
title: string
|
||||
caption?: string
|
||||
imageSrc: string
|
||||
imageAlt: string
|
||||
imageSrcSet: string
|
||||
href: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
meta: IMeta
|
||||
data: CardData[]
|
||||
}
|
||||
|
||||
const Index: NextPage<Props> = props => {
|
||||
if (!props.data) {
|
||||
console.log('collective kek error')
|
||||
return
|
||||
}
|
||||
|
||||
const src = 'https://art.shlisselburg.org/static/img/collective2019.jpg'
|
||||
const srcSet = imageSrcSet(src, [ImageSize.big, ImageSize.large, ImageSize.medium, ImageSize.small])
|
||||
|
||||
return (
|
||||
<App
|
||||
showAuthor={true}
|
||||
wide={true}
|
||||
>
|
||||
<Head>
|
||||
<title>{props.title}</title>
|
||||
<Meta meta={props.meta} />
|
||||
</Head>
|
||||
|
||||
<Image
|
||||
src={src}
|
||||
srcSet={srcSet}
|
||||
style={{
|
||||
marginBottom: 'var(--size-l)',
|
||||
borderRadius: 'var(--radius)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<CardGrid style={{
|
||||
marginBottom: 'var(--size-xl)',
|
||||
}}>
|
||||
{props.data.map(item => (
|
||||
<Card
|
||||
key={item.href}
|
||||
layout={'featured'}
|
||||
img={{
|
||||
src: item.imageSrc,
|
||||
alt: item.imageAlt,
|
||||
srcSet: item.imageSrcSet,
|
||||
}}
|
||||
href={item.href}
|
||||
>
|
||||
<Block direction={'vertical'}>
|
||||
<span>{item.title}</span>
|
||||
<span style={{
|
||||
paddingTop: 'var(--size-s)',
|
||||
fontSize: 'var(--font-size-second)',
|
||||
}}>
|
||||
{item.caption}
|
||||
</span>
|
||||
</Block>
|
||||
</Card>
|
||||
))}
|
||||
</CardGrid>
|
||||
</App>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps = async (ctx: NextPageContext) => {
|
||||
const title = 'Преподаватели Шлиссельбургской ДХШ'
|
||||
const pages = await Promise.all(persons
|
||||
.map(page => requestGet<IPage>(createApiUrl(`/api/page?page=${page}`), null))
|
||||
)
|
||||
const data: CardData[] = pages
|
||||
.filter(Boolean)
|
||||
.map(x => {
|
||||
return {
|
||||
title: x.title,
|
||||
caption: x.description,
|
||||
href: x.url,
|
||||
imageAlt: title,
|
||||
imageSrc: x.cover.src,
|
||||
imageSrcSet: imageSrcSet(x.cover.src, [ImageSize.medium, ImageSize.small])
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
data,
|
||||
title,
|
||||
meta: meta({
|
||||
title,
|
||||
description: 'Преподаватели Шлиссельбургской ДХШ',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Index
|
||||
+26
-36
@@ -4,9 +4,10 @@ import { Meta } from 'src/components/Meta'
|
||||
import { PageGrid } from 'src/components/PageGrid'
|
||||
import { HudozkaTitle } from 'src/components/HudozkaTitle'
|
||||
import { MetaBuilder } from 'src/lib/meta'
|
||||
import { getPagesCardsByTags } from 'src/rest'
|
||||
import { NextPage } from 'next'
|
||||
import { IMeta, PageCardData, PageCardDto } from 'src/types'
|
||||
import { IMeta, PageCardDto } from 'src/types'
|
||||
import { apiGet } from '@/next-lib'
|
||||
import { createHomeCards } from '@/remote/factory'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
@@ -14,44 +15,33 @@ type Props = {
|
||||
items: PageCardDto[]
|
||||
}
|
||||
|
||||
const Index: NextPage<Props> = props => {
|
||||
const items = props.items.map<PageCardData>(item => ({
|
||||
url: item.url,
|
||||
title: item.title,
|
||||
featured: item.featured,
|
||||
date: new Date(item.date),
|
||||
coverSrc: item.cover?.src,
|
||||
}))
|
||||
const Index: NextPage<Props> = props => (
|
||||
<App
|
||||
showAuthor={true}
|
||||
wide={false}
|
||||
>
|
||||
<Head>
|
||||
<title>{props.title}</title>
|
||||
<Meta meta={props.meta} />
|
||||
</Head>
|
||||
|
||||
return (
|
||||
<App
|
||||
showAuthor={true}
|
||||
wide={true}
|
||||
>
|
||||
<Head>
|
||||
<title>{props.title}</title>
|
||||
<Meta meta={props.meta} />
|
||||
</Head>
|
||||
<HudozkaTitle
|
||||
style={{
|
||||
marginTop: 'var(--size-m)',
|
||||
marginBottom: 'var(--size-m)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<HudozkaTitle
|
||||
style={{
|
||||
marginBottom: 'var(--size-l)'
|
||||
}}
|
||||
/>
|
||||
|
||||
<PageGrid
|
||||
items={items}
|
||||
/>
|
||||
</App>
|
||||
)
|
||||
}
|
||||
<PageGrid
|
||||
items={props.items}
|
||||
/>
|
||||
</App>
|
||||
)
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
const items = await getPagesCardsByTags([
|
||||
'event',
|
||||
'album',
|
||||
'post',
|
||||
])
|
||||
const url = 'https://hudozka.tmshv.com/home'
|
||||
const items = await apiGet(createHomeCards)(url, [])
|
||||
|
||||
const title = 'Шлиссельбургская ДХШ'
|
||||
const meta = (new MetaBuilder())
|
||||
.setTitle(title)
|
||||
|
||||
@@ -21,10 +21,9 @@ export interface IAppProps {
|
||||
breadcrumbs?: IBreadcumbsPart[]
|
||||
}
|
||||
|
||||
export const App: React.FC<IAppProps> = ({ showAuthor = false, ...props }) => {
|
||||
export const App: React.FC<IAppProps> = ({ showAuthor = false, wide = false, ...props }) => {
|
||||
const hideBreadcrumbs = useMobile()
|
||||
const router = useRouter()
|
||||
const wide = props.wide ?? false
|
||||
const blockStyle = {
|
||||
justifyContent: 'center'
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import s from './styles.module.css'
|
||||
|
||||
import cx from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import { Image } from '../Image'
|
||||
|
||||
const layoutClass = {
|
||||
simple: s.simple,
|
||||
@@ -13,11 +12,7 @@ export type CardLayout = 'simple' | 'featured'
|
||||
export type CardProps = {
|
||||
style?: React.CSSProperties
|
||||
href: string
|
||||
img: {
|
||||
alt: string
|
||||
src: string
|
||||
srcSet: string
|
||||
}
|
||||
cover: React.ReactNode
|
||||
layout: CardLayout
|
||||
}
|
||||
|
||||
@@ -26,11 +21,7 @@ export const Card: React.FC<CardProps> = props => (
|
||||
<Link href={props.href}>
|
||||
<a>
|
||||
<div className={s.image}>
|
||||
<Image
|
||||
alt={props.img.alt}
|
||||
src={props.img.src}
|
||||
srcSet={props.img.srcSet}
|
||||
/>
|
||||
{props.cover}
|
||||
</div>
|
||||
|
||||
<div className={s.body}>
|
||||
|
||||
@@ -31,18 +31,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
|
||||
transition: all 150ms ease-out;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
background-color: var(--color-back);
|
||||
padding: var(--size-m);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.grid {
|
||||
--min-cell-size: 240px;
|
||||
--min-cell-size: 270px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--min-cell-size), 1fr));
|
||||
grid-auto-rows: 25em;
|
||||
grid-auto-flow: dense;
|
||||
grid-gap: var(--size-xl) var(--size-l);
|
||||
grid-gap: var(--size-l) var(--size-l);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 31.25em) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import s from './title.module.css'
|
||||
|
||||
import { memo } from 'react'
|
||||
|
||||
export type HudozkaTitleProps = {
|
||||
@@ -5,33 +7,9 @@ export type HudozkaTitleProps = {
|
||||
}
|
||||
|
||||
export const HudozkaTitle: React.FC<HudozkaTitleProps> = memo(props => (
|
||||
<div style={props.style}>
|
||||
<style jsx>{`
|
||||
div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
font-size: var(--font-size-h2);
|
||||
}
|
||||
|
||||
@media (max-width: 31.25em) {
|
||||
div {
|
||||
--padding: 0 0 2em;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<h1>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 24,
|
||||
}}>МБУДО</span>
|
||||
|
||||
Шлиссельбургская <br />
|
||||
<span>
|
||||
детская художественная школа
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<h1 style={props.style} className={s.title}>
|
||||
<span className={s.prefix}>МБУДО</span>
|
||||
Шлиссельбургская <br />
|
||||
<span className={s.suffix}>детская художественная школа</span>
|
||||
</h1>
|
||||
))
|
||||
@@ -0,0 +1,19 @@
|
||||
.title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title .suffix {
|
||||
font-size: var(--font-size-h2);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
font-size: var(--font-size-h2);
|
||||
display: block;
|
||||
margin-bottom: var(--size-s);
|
||||
}
|
||||
|
||||
@media (max-width: 31.25em) {
|
||||
.title {
|
||||
--padding: 0 0 2em;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import s from './styles.module.css'
|
||||
|
||||
import { Share } from '../Share'
|
||||
import { PageMeta } from '../PageMeta'
|
||||
import { ITag } from 'src/types'
|
||||
|
||||
@@ -13,8 +12,6 @@ export type PageProps = {
|
||||
export const Page: React.FC<PageProps> = props => (
|
||||
<div className={s.page} style={props.style}>
|
||||
{props.children}
|
||||
<hr />
|
||||
<Share />
|
||||
|
||||
<PageMeta
|
||||
style={{
|
||||
|
||||
@@ -1,87 +1,64 @@
|
||||
import { ImageSize, PageCardData } from 'src/types'
|
||||
import Image from 'next/image'
|
||||
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 { imageSrcSet, imageSrc } from 'src/lib/image'
|
||||
import { Button } from '../Button'
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
export type PageGridProps = {
|
||||
items: PageCardData[]
|
||||
items: PageCardDto[]
|
||||
}
|
||||
|
||||
export const PageGrid: React.FC<PageGridProps> = props => {
|
||||
const [page, setPage] = useState(1)
|
||||
const i = page * 24
|
||||
const items = props.items.slice(0, i)
|
||||
const onMore = useCallback(() => {
|
||||
setPage(x => x + 1)
|
||||
}, [])
|
||||
const hasMore = i < props.items.length
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardGrid
|
||||
style={{
|
||||
marginBottom: 'var(--size-m)',
|
||||
}}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
const gridColumn = item.featured ? 'span 2' : 'auto'
|
||||
const layout: CardLayout = item.featured ? 'featured' : 'simple'
|
||||
const cover = item.coverSrc ?? process.env.APP_CARD_DEFAULT_IMAGE
|
||||
const src = cover
|
||||
const srcSet = imageSrcSet(src, [ImageSize.large, ImageSize.medium, ImageSize.small])
|
||||
<CardGrid
|
||||
style={{
|
||||
marginBottom: 'var(--size-xl)',
|
||||
}}
|
||||
>
|
||||
{props.items.map(item => {
|
||||
const gridColumn = item.featured ? 'span 2' : 'auto'
|
||||
const layout: CardLayout = item.featured ? 'featured' : 'simple'
|
||||
|
||||
const content = !item.featured
|
||||
? (
|
||||
<Block
|
||||
direction={'vertical'}
|
||||
>
|
||||
{item.title}
|
||||
<Spacer />
|
||||
const content = !item.featured
|
||||
? (
|
||||
<Block
|
||||
direction={'vertical'}
|
||||
>
|
||||
{item.title}
|
||||
<Spacer />
|
||||
{!item.date ? null : (
|
||||
<Date style={{
|
||||
marginTop: 'var(--size-m)',
|
||||
}}>{item.date}</Date>
|
||||
</Block>
|
||||
) : (
|
||||
item.title
|
||||
)
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={i}
|
||||
href={item.url}
|
||||
img={{
|
||||
alt: item.title,
|
||||
src,
|
||||
srcSet,
|
||||
}}
|
||||
layout={layout}
|
||||
style={{
|
||||
gridColumn,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Card>
|
||||
)}
|
||||
</Block>
|
||||
) : (
|
||||
item.title
|
||||
)
|
||||
})}
|
||||
</CardGrid>
|
||||
<Block direction={'horizontal'} style={{
|
||||
marginTop: 'var(--size-l)'
|
||||
}}>
|
||||
<Spacer />
|
||||
<Button
|
||||
disabled={!hasMore}
|
||||
onClick={onMore}
|
||||
style={{
|
||||
flex: 2,
|
||||
}}
|
||||
>Загрузить ещё</Button>
|
||||
<Spacer />
|
||||
</Block>
|
||||
</>
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={item.id}
|
||||
href={item.url}
|
||||
cover={(
|
||||
<Image
|
||||
layout={'fill'}
|
||||
objectFit={'cover'}
|
||||
src={item.cover.src}
|
||||
alt={item.cover.alt}
|
||||
/>
|
||||
)}
|
||||
layout={layout}
|
||||
style={{
|
||||
gridColumn,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</CardGrid>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export type YoutubeProps = {
|
||||
url: string
|
||||
}
|
||||
|
||||
export const Youtube: React.FC<{ url: string }> = props => {
|
||||
const url = new URL(props.url)
|
||||
const videoId = url.searchParams.get('v')
|
||||
if (!videoId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const src = `//www.youtube.com/embed/${videoId}`
|
||||
|
||||
return (
|
||||
<div className="kazimir__video">
|
||||
<iframe
|
||||
src={src}
|
||||
frameBorder="0"
|
||||
// allowFullscreen
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+3
-25
@@ -1,36 +1,14 @@
|
||||
import { ImageSize } from 'src/types'
|
||||
|
||||
export type ResizeOptions = {
|
||||
width: number
|
||||
height: number
|
||||
n?: number
|
||||
}
|
||||
|
||||
export function getResizedUrl(src: string, options: ResizeOptions): string {
|
||||
const n = options.n ?? 1
|
||||
if (!src || src === '') {
|
||||
return null
|
||||
}
|
||||
|
||||
return `https://images.weserv.nl/?url=${src}&w=${options.width}&h=${options.height}&n=-1`
|
||||
return `http://images.weserv.nl/?url=${src}&w=${options.width}&h=${options.height}&n=${n}`
|
||||
}
|
||||
|
||||
export function imageSrcSet(src: string, sizes: ImageSize[]): string {
|
||||
const set = sizes
|
||||
.map(x => ({
|
||||
value: x,
|
||||
href: getResizedUrl(src, {
|
||||
width: x,
|
||||
height: x,
|
||||
}),
|
||||
}))
|
||||
|
||||
return set
|
||||
.map(x => `${x.href} ${x.value}w`)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
export function imageSrc(src: string, size: ImageSize): string {
|
||||
return getResizedUrl(src, {
|
||||
width: size,
|
||||
height: size,
|
||||
})
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { TextToken, Token } from 'src/types'
|
||||
|
||||
export function joinTextTokens(tokens: TextToken[]): TextToken {
|
||||
return {
|
||||
token: 'text',
|
||||
data: tokens
|
||||
.map(x => x.data)
|
||||
.join('\n\n'),
|
||||
}
|
||||
}
|
||||
|
||||
export function joinTokens(tokens: Token[]): Token[] {
|
||||
let buffer: TextToken[] = []
|
||||
const result: Token[] = []
|
||||
|
||||
for (const token of tokens) {
|
||||
if (token.token === 'text') {
|
||||
buffer.push(token)
|
||||
continue
|
||||
}
|
||||
|
||||
const compiledToken = joinTextTokens(buffer)
|
||||
buffer = []
|
||||
|
||||
result.push(compiledToken)
|
||||
result.push(token)
|
||||
}
|
||||
|
||||
if (buffer.length) {
|
||||
result.push(
|
||||
joinTextTokens(buffer)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -36,3 +36,14 @@ export function wrapInitialProps(fn: (ctx: NextPageContext) => void) {
|
||||
export function createApiUrl(path: string) {
|
||||
return `https://api.tmshv.com/hudozka${path}`
|
||||
}
|
||||
|
||||
export function apiGet<I, O>(factory: (response: I) => O) {
|
||||
return async (url: string, defaultResponse: O) => {
|
||||
const res = await requestGet<I>(url, null)
|
||||
if (!res) {
|
||||
return defaultResponse
|
||||
}
|
||||
|
||||
return factory(res)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import { getResizedUrl } from "@/lib/image";
|
||||
import { IPage, ITag, PageCardDto, Token } from "@/types";
|
||||
import { asItem } from "./lib";
|
||||
import { StrapiComponentEmbed, StrapiComponent, StrapiHome, StrapiPage, StrapiPageCard } from "./types";
|
||||
|
||||
export function createPageUrls(pages: StrapiPage[]) {
|
||||
return {
|
||||
items: pages.map(page => page.slug),
|
||||
}
|
||||
}
|
||||
|
||||
export function isYoutubeUrl(url: string): boolean {
|
||||
return /youtube\.com/.test(url)
|
||||
}
|
||||
|
||||
export function createEmbed(component: StrapiComponentEmbed): Token {
|
||||
if (isYoutubeUrl(component.src)) {
|
||||
return {
|
||||
token: 'youtube',
|
||||
data: {
|
||||
url: component.src,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
token: 'html',
|
||||
data: `<iframe src="${component.src}" width="100%" height="480" frameborder="0"></iframe>`
|
||||
}
|
||||
}
|
||||
|
||||
export function createPageTokens(components: StrapiComponent[]): Token[] {
|
||||
return components.map(component => {
|
||||
switch (component.__component) {
|
||||
case 'hudozka.text': {
|
||||
return {
|
||||
token: 'text',
|
||||
data: component.text,
|
||||
}
|
||||
}
|
||||
|
||||
case 'hudozka.image': {
|
||||
return {
|
||||
token: 'image',
|
||||
data: {
|
||||
alt: component.caption,
|
||||
caption: component.caption,
|
||||
src: component.media.url,
|
||||
width: component.media.width,
|
||||
height: component.media.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case 'hudozka.document': {
|
||||
return {
|
||||
token: 'file',
|
||||
data: {
|
||||
url: component.media.url,
|
||||
slug: 'jopa',
|
||||
image_url: getResizedUrl(component.media.url, {
|
||||
width: 200,
|
||||
height: 200,
|
||||
n: 1,
|
||||
}),
|
||||
file_url: component.media.url,
|
||||
title: component.title,
|
||||
file_size: component.media.size * 1000,
|
||||
// file_size: component.media.size,
|
||||
file_format: component.media.mime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case 'hudozka.embed': {
|
||||
return createEmbed(component)
|
||||
}
|
||||
|
||||
case 'hudozka.card-grid': {
|
||||
return {
|
||||
token: 'grid',
|
||||
data: {
|
||||
items: component.items
|
||||
.filter(Boolean)
|
||||
.map(createCardGrid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
token: 'text',
|
||||
data: `
|
||||
${JSON.stringify(component)}
|
||||
`,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function createPage(res: StrapiPage | StrapiPage[]): IPage {
|
||||
const item = asItem(res)
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
const tags: ITag[] = item.tags.map(tag => ({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
slug: tag.slug,
|
||||
href: `/tags/${tag.slug}`,
|
||||
}))
|
||||
|
||||
return {
|
||||
title: item.title,
|
||||
description: item.excerpt,
|
||||
id: `${item.id}`,
|
||||
url: item.slug,
|
||||
|
||||
data: '',
|
||||
date: '',
|
||||
cover: {
|
||||
src: item.cover.url,
|
||||
width: item.cover.width,
|
||||
height: item.cover.height,
|
||||
},
|
||||
tokens: [
|
||||
{
|
||||
token: 'text',
|
||||
data: `# ${item.title}`,
|
||||
},
|
||||
|
||||
...createPageTokens(item.content)
|
||||
.filter(Boolean),
|
||||
],
|
||||
tags,
|
||||
// breadcrumb?: [],
|
||||
featured: false,
|
||||
}
|
||||
}
|
||||
|
||||
function isCardFeatured(card: StrapiPageCard): boolean {
|
||||
return card.layout === 'big' || card.layout === 'medium'
|
||||
}
|
||||
|
||||
export function createCardGrid(card: StrapiPageCard): PageCardDto {
|
||||
// const cover = item.coverSrc ?? process.env.APP_CARD_DEFAULT_IMAGE
|
||||
|
||||
return {
|
||||
id: card.id,
|
||||
// id: card.page.id,
|
||||
url: card.page.slug,
|
||||
title: card.page.title,
|
||||
featured: isCardFeatured(card),
|
||||
date: card.page.date,
|
||||
cover: {
|
||||
src: card.page.cover.url,
|
||||
width: card.page.cover.width,
|
||||
height: card.page.cover.height,
|
||||
alt: card.page.title,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createHomeCards(data: StrapiHome): PageCardDto[] {
|
||||
// const cover = item.coverSrc ?? process.env.APP_CARD_DEFAULT_IMAGE
|
||||
|
||||
return data.cards.map(createCardGrid)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function asArray<T>(value: T | T[]) {
|
||||
return Array.isArray(value)
|
||||
? value
|
||||
: [value]
|
||||
}
|
||||
|
||||
export function asItem<T>(value: T | T[]) {
|
||||
return Array.isArray(value)
|
||||
? value[0]
|
||||
: value
|
||||
}
|
||||
|
||||
export function paramsToSlug(parts: string | string[]): string {
|
||||
const slug = asArray(parts).join('/')
|
||||
|
||||
return `/${slug}`
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
type StrapiMediaObject = {
|
||||
url: string,
|
||||
name: string,
|
||||
hash: string,
|
||||
ext: string,
|
||||
mime: string,
|
||||
width: number,
|
||||
height: number,
|
||||
size: number,
|
||||
// "path": null,
|
||||
}
|
||||
|
||||
type StrapiMedia = StrapiMediaObject & {
|
||||
id: number,
|
||||
alternativeText: string | null,
|
||||
caption: string | null,
|
||||
// "previewUrl": null,
|
||||
// "provider": "digitalocean",
|
||||
// "provider_metadata": null,
|
||||
// "created_at": "2021-01-31T20:55:04.027Z",
|
||||
// "updated_at": "2021-01-31T20:55:04.027Z"
|
||||
formats: null | {
|
||||
thumbnail: StrapiMediaObject,
|
||||
large: StrapiMediaObject,
|
||||
medium: StrapiMediaObject,
|
||||
small: StrapiMediaObject,
|
||||
},
|
||||
}
|
||||
|
||||
type StrapiComponentText = {
|
||||
__component: "hudozka.text",
|
||||
id: number,
|
||||
text: string,
|
||||
}
|
||||
|
||||
type StrapiComponentImage = {
|
||||
__component: "hudozka.image",
|
||||
id: number,
|
||||
wide: boolean,
|
||||
caption: string,
|
||||
media: StrapiMedia,
|
||||
}
|
||||
|
||||
type StrapiComponentDocument = {
|
||||
__component: "hudozka.document",
|
||||
id: number,
|
||||
title: string,
|
||||
media: StrapiMediaObject
|
||||
// "media": {
|
||||
// "id": 210,
|
||||
// "name": "Положение.pdf",
|
||||
// // "alternativeText": null,
|
||||
// // "caption": null,
|
||||
// // "width": null,
|
||||
// // "height": null,
|
||||
// // "formats": null,
|
||||
// // "hash": "Polozhenie_efaf157737",
|
||||
// // "ext": ".pdf",
|
||||
// // "mime": "application/pdf",
|
||||
// // "size": 103.79,
|
||||
// "url": "https://hudozkacdn.tmshv.com/Polozhenie_efaf157737.pdf",
|
||||
// // "previewUrl": null,
|
||||
// // "provider": "digitalocean",
|
||||
// // "provider_metadata": null,
|
||||
// // "created_at": "2021-01-31T21:14:03.158Z",
|
||||
// // "updated_at": "2021-01-31T21:14:03.158Z",
|
||||
// }
|
||||
}
|
||||
|
||||
export type StrapiComponentEmbed = {
|
||||
__component: "hudozka.embed",
|
||||
id: number,
|
||||
src: string,
|
||||
}
|
||||
|
||||
export type StrapiComponentCardGrid = {
|
||||
__component: "hudozka.card-grid",
|
||||
id: number,
|
||||
items: StrapiPageCard[],
|
||||
}
|
||||
|
||||
export type StrapiComponent =
|
||||
| StrapiComponentText
|
||||
| StrapiComponentImage
|
||||
| StrapiComponentDocument
|
||||
| StrapiComponentEmbed
|
||||
| StrapiComponentCardGrid
|
||||
|
||||
export type StrapiTag = {
|
||||
id: number,
|
||||
slug: string,
|
||||
name: string,
|
||||
// "page": 114,
|
||||
// "created_at": "2021-01-31T20:38:39.373Z",
|
||||
// "updated_at": "2021-02-17T07:37:54.749Z"
|
||||
}
|
||||
|
||||
export type StrapiPage = {
|
||||
id: number,
|
||||
title: string,
|
||||
excerpt: string,
|
||||
slug: string,
|
||||
date?: string,
|
||||
cover?: StrapiMedia,
|
||||
published_at: string,
|
||||
created_at: string,
|
||||
updated_at: string,
|
||||
content: StrapiComponent[],
|
||||
tags: StrapiTag[],
|
||||
// "breadcrumbs": [],
|
||||
// "breadcrumbRelations": []
|
||||
}
|
||||
|
||||
export type StrapiHome = {
|
||||
id: number,
|
||||
title: string
|
||||
cards: StrapiPageCard[]
|
||||
// "created_at": "2021-02-04T04:12:38.291Z",
|
||||
// "updated_at": "2021-02-04T16:28:30.961Z",
|
||||
}
|
||||
|
||||
export type StrapiPageCard = {
|
||||
__component: 'hudozka.page-card',
|
||||
id: number,
|
||||
page: StrapiPage,
|
||||
layout: 'small' | 'medium' | 'big',
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import { createApiUrl, requestGet, IResponseItems } from './next-lib'
|
||||
import { IPage, PageCardData, PageCardDto } from './types'
|
||||
import { queryList } from './lib/url'
|
||||
|
||||
export type QuerySlice = {
|
||||
skip: number
|
||||
limit: number
|
||||
sortBy: string
|
||||
fields?: string[]
|
||||
}
|
||||
|
||||
export async function getPagesByTags<T = IPage>(tags: string[], { skip, limit, sortBy, fields }: QuerySlice) {
|
||||
const tag = queryList('tag', tags)
|
||||
const field = queryList('field', fields ?? [])
|
||||
|
||||
const url = `/api/pages/tags?${tag}&${field}&skip=${skip}&limit=${limit}&sortBy=${sortBy}`
|
||||
const res = await requestGet<IResponseItems<T>>(createApiUrl(url), null)
|
||||
|
||||
return res?.items
|
||||
}
|
||||
|
||||
export async function getPagesCardsByTags(tags: string[]): Promise<PageCardDto[]> {
|
||||
const skip = 0
|
||||
const limit = 1000
|
||||
const fields = [
|
||||
'url',
|
||||
'title',
|
||||
'featured',
|
||||
'date',
|
||||
'cover'
|
||||
]
|
||||
|
||||
const items = await getPagesByTags<PageCardDto>(tags, { fields, skip, limit, sortBy: '-date' })
|
||||
if (!Array.isArray(items)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
.kazimir__image {
|
||||
position: relative;
|
||||
width: calc(100% + var(--kazimir-image-increase));
|
||||
margin-left: calc(-0.5 * var(--kazimir-image-increase));
|
||||
margin-bottom: var(--size-m);
|
||||
/* position: relative; */
|
||||
/* width: calc(100% + var(--kazimir-image-increase)); */
|
||||
/* margin-left: calc(-0.5 * var(--kazimir-image-increase)); */
|
||||
margin-bottom: var(--size-l);
|
||||
}
|
||||
|
||||
.kazimir__image p {
|
||||
@@ -10,15 +10,17 @@
|
||||
}
|
||||
|
||||
.kazimir__image img {
|
||||
border-radius: var(--kazimir-radius);
|
||||
border-radius: var(--radius);
|
||||
|
||||
max-width: 100%;
|
||||
max-height: 35em;
|
||||
}
|
||||
|
||||
.kazimir__image figcaption {
|
||||
color: var(--color-text-second);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-second);
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
padding-top: var(--size-xs);
|
||||
text-align: center;
|
||||
}
|
||||
+4
-5
@@ -14,7 +14,7 @@
|
||||
|
||||
/* COLOR */
|
||||
--color-text: hsl(0, 0%, 15%);
|
||||
--color-text-second: hsl(240, 10%, 70%);
|
||||
--color-text-second: hsl(240, 10%, 60%);
|
||||
--color-text-opposite: hsl(0, 0%, 95%);
|
||||
|
||||
--color-link: hsl(0, 0%, 85%);
|
||||
@@ -46,14 +46,13 @@
|
||||
--embed-video-ratio: 56.25%;
|
||||
|
||||
/* CONTENT */
|
||||
--content-width: 60%;
|
||||
--content-width: 75%;
|
||||
--content-margin: 0;
|
||||
--content-wide-width: 100%;
|
||||
--content-wide-margin: var(--size-l) var(--size-xl);
|
||||
|
||||
/* KAZIMIR */
|
||||
--kazimir-caption-margin: 0px;
|
||||
--kazimir-radius: 10px;
|
||||
--kazimir-image-increase: 50px;
|
||||
}
|
||||
|
||||
@@ -160,8 +159,8 @@ a.invisible {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
/* TEXT COLOR */
|
||||
--color-text: hsl(240, 10%, 80%);
|
||||
--color-text-second: hsl(240, 10%, 40%);
|
||||
--color-text: hsl(240, 10%, 90%);
|
||||
--color-text-second: hsl(240, 10%, 90%);
|
||||
--color-text-opposite: hsl(240, 10%, 15%);
|
||||
|
||||
--color-back-main: hsl(240, 10%, 10%);
|
||||
|
||||
+22
-19
@@ -8,15 +8,8 @@ export interface IBreadcumbsPart {
|
||||
href: string
|
||||
}
|
||||
|
||||
export type PageCardData = {
|
||||
url: string
|
||||
title: string
|
||||
date: Date
|
||||
featured?: boolean
|
||||
coverSrc?: string
|
||||
}
|
||||
|
||||
export type PageCardDto = {
|
||||
id: number
|
||||
url: string
|
||||
title: string
|
||||
featured: boolean
|
||||
@@ -85,23 +78,17 @@ export interface IImage {
|
||||
}
|
||||
|
||||
export interface ITag {
|
||||
id: number
|
||||
name: string
|
||||
slug: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export enum ImageSize {
|
||||
big = 3000,
|
||||
large = 1500,
|
||||
medium = 1000,
|
||||
small = 500,
|
||||
thumbnail = 200,
|
||||
}
|
||||
|
||||
export type ImageDefinition = {
|
||||
width: number
|
||||
height: number
|
||||
src: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
export type TextToken = {
|
||||
@@ -115,6 +102,8 @@ export type ImageToken = {
|
||||
src: string
|
||||
alt: string
|
||||
caption: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +113,8 @@ export type FileTokenData = {
|
||||
image_url: string
|
||||
file_url: string
|
||||
title: string
|
||||
file_size: number
|
||||
file_format: 'application/pdf'
|
||||
file_size?: number
|
||||
file_format: string
|
||||
}
|
||||
|
||||
export type FileToken = {
|
||||
@@ -153,4 +142,18 @@ export type InstagramToken = {
|
||||
}
|
||||
}
|
||||
|
||||
export type Token = TextToken | ImageToken | FileToken | HtmlToken | YoutubeToken | InstagramToken
|
||||
export type GridToken = {
|
||||
token: 'grid'
|
||||
data: {
|
||||
items: PageCardDto[]
|
||||
}
|
||||
}
|
||||
|
||||
export type Token =
|
||||
| TextToken
|
||||
| ImageToken
|
||||
| FileToken
|
||||
| HtmlToken
|
||||
| YoutubeToken
|
||||
| InstagramToken
|
||||
| GridToken
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"target": "es5",
|
||||
"target": "es6",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
|
||||
Reference in new issue
Block a user