Merge pull request #67 from tmshv/upd/strapi-integration

Strapi integration
This commit is contained in:
tmshv authored and GitHub committed 2021-02-17 19:49:00 +03:00
commit aa21c21dfb
29 files changed
+1588 -4957

No files matched your search

-5
View File
@@ -1,5 +0,0 @@
{
"presets": [
"next/babel"
]
}
+2 -1
View File
@@ -19,4 +19,5 @@ dist
/coverage
**/.vscode
*.log
.now
.now
.vercel
+7 -1
View File
@@ -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',
],
},
}
+1020 -4451
View File
File diff suppressed because it is too large. Load diff
+6 -6
View File
@@ -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
View File
@@ -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('/'))
-7
View File
@@ -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>
-138
View File
@@ -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
View File
@@ -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)
+1 -2
View File
@@ -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 -11
View File
@@ -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}>
-12
View File
@@ -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);
+2 -2
View File
@@ -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) {
+7 -29
View File
@@ -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;
}
}
-3
View File
@@ -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={{
+46 -69
View File
@@ -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>
)
}
+23
View File
@@ -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
View File
@@ -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,
})
}
-36
View File
@@ -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
}
+11
View File
@@ -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)
}
}
+169
View File
@@ -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)
}
+17
View File
@@ -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}`
}
+127
View File
@@ -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
View File
@@ -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
}
+8 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -6,7 +6,7 @@
"src/*"
]
},
"target": "es5",
"target": "es6",
"lib": [
"dom",
"dom.iterable",